diff options
Diffstat (limited to 'vendor/gipfl')
297 files changed, 34708 insertions, 0 deletions
diff --git a/vendor/gipfl/calendar/composer.json b/vendor/gipfl/calendar/composer.json new file mode 100644 index 0000000..5e84ef6 --- /dev/null +++ b/vendor/gipfl/calendar/composer.json @@ -0,0 +1,32 @@ +{ + "name": "gipfl/calendar", + "type": "library", + "description": "Calendar Utils", + "keywords": ["calendar"], + "homepage": "https://github.com/gipfl/calendar", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "require": { + "php": ">=5.4.0", + "gipfl/format": ">=0.3", + "gipfl/icingaweb2": ">=0.4.0", + "gipfl/translation": ">=0.1.1" + }, + "autoload": { + "psr-4": { + "gipfl\\Calendar\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "gipfl\\Tests\\Calendar\\": "tests" + } + } +} diff --git a/vendor/gipfl/calendar/src/Calendar.php b/vendor/gipfl/calendar/src/Calendar.php new file mode 100644 index 0000000..e81227c --- /dev/null +++ b/vendor/gipfl/calendar/src/Calendar.php @@ -0,0 +1,246 @@ +<?php + +namespace gipfl\Calendar; + +use gipfl\Format\LocalTimeFormat; +use InvalidArgumentException; + +class Calendar +{ + const FIRST_IS_MONDAY = 1; + const FIRST_IS_SUNDAY = 0; + + protected $firstOfWeek; + + protected $weekDays = []; + + protected $shortWeekDays = []; + + protected $timeFormat; + + public function __construct($firstOfWeek = self::FIRST_IS_MONDAY) + { + $this->timeFormat = new LocalTimeFormat(); + $this->setFirstOfWeek($firstOfWeek); + } + + public function firstOfWeekIsMonday() + { + return $this->firstOfWeek === self::FIRST_IS_MONDAY; + } + + public function firstOfWeekIsSunday() + { + return $this->firstOfWeek === self::FIRST_IS_SUNDAY; + } + + public function setFirstOfWeek($firstOfWeek) + { + if ($firstOfWeek === self::FIRST_IS_SUNDAY || $firstOfWeek === self::FIRST_IS_MONDAY) { + if ($firstOfWeek !== $this->firstOfWeek) { + $this->firstOfWeek = $firstOfWeek; + $this->prepareWeekDays(); + } + + return $this; + } else { + throw new InvalidArgumentException( + "First day of week has to be either 0 or 1, got '$firstOfWeek'" + ); + } + } + + protected function prepareWeekDays() + { + if ($this->firstOfWeekIsSunday()) { + $start = '2019-02-03'; + } else { + $start = '2019-02-04'; + } + + for ($i = 0; $i < 7; $i++) { + $day = strtotime("$start +{$i}days"); + $this->weekDays[] = $this->timeFormat->getWeekdayName($day); + $this->shortWeekDays[] = $this->timeFormat->getShortWeekdayName($day); + } + } + + public function listWeekDayNames() + { + return $this->weekDays; + } + + public function listShortWeekDayNames() + { + return $this->shortWeekDays; + } + + /** + * Either 'N' or 'w', depending on the first day of week + * + * @return string + */ + protected function getDowFormat() + { + if ($this->firstOfWeekIsMonday()) { + // N -> 1-7 (Mo-Su) + return 'N'; + } else { + // w -> 0-6 (Su-Sa) + return 'w'; + } + } + + /** + * @param $time + * @return int + */ + protected function getWeekDay($time) + { + return (int) date($this->getDowFormat(), $time); + } + + /** + * @param int $now + * @return array + */ + public function getDaysForWeek($now) + { + $formatDow = $this->getDowFormat(); + $today = date('Y-m-d', $now); + $day = $this->getFirstDayOfWeek($today); + $weekday = (int) date($formatDow, strtotime($day)); + $week = [$weekday => $day]; + for ($i = 1; $i < 7; $i++) { + $day = date('Y-m-d', strtotime("$day +1day")); + $weekday = (int) date($formatDow, strtotime($day)); + $week[$weekday] = $day; + } + + return $week; + } + + /** + * @param int $now + * @return array + */ + public function getWorkingDaysForWeek($now) + { + $formatDow = $this->getDowFormat(); + $today = date('Y-m-d', $now); + $day = $this->getFirstDayOfWeek($today, self::FIRST_IS_MONDAY); + $weekday = (int) date($formatDow, strtotime($day)); + $week = [$weekday => $day]; + for ($i = 1; $i < 5; $i++) { + $day = date('Y-m-d', strtotime("$day +1day")); + $weekday = (int) date($formatDow, strtotime($day)); + $week[$weekday] = $day; + } + + return $week; + } + + /** + * @param string $day + * @param int $firstOfWeek + * @return string + */ + public function getFirstDayOfWeek($day, $firstOfWeek = null) + { + if ($firstOfWeek === null) { + $firstOfWeek = $this->firstOfWeek; + } + $dow = $this->getWeekDay(strtotime($day)); + if ($dow > $firstOfWeek) { + $sub = $dow - $firstOfWeek; + return date('Y-m-d', strtotime("$day -{$sub}day")); + } else { + return $day; + } + } + + /** + * @param string $day + * @param int $firstOfWeek + * @return string + */ + protected function getLastDayOfWeek($day, $firstOfWeek = null) + { + if ($firstOfWeek === null) { + $firstOfWeek = $this->firstOfWeek; + } + $dow = $this->getWeekDay(strtotime($day)); + $lastOfWeek = $firstOfWeek + 6; + if ($dow < $lastOfWeek) { + $add = $lastOfWeek - $dow; + return static::expressionToDate(static::incDay($day, $add)); + } else { + return $day; + } + } + + public function getWeekOfTheYear($day) + { + $time = strtotime($day); + // 0 = Sunday + if ($this->firstOfWeekIsSunday() && $this->getWeekDay($time) === 0) { + if (substr($time, 4, 6) === '-12-31') { + return (int) date('W', strtotime(static::decDay($day))); + } else { + return (int) date('W', strtotime(static::incDay($day))); + } + } else { + return (int) date('W', $time); + } + } + + /** + * @param int $now + * @return array + */ + public function getWeeksForMonth($now) + { + $first = date('Y-m-01', $now); + $last = date('Y-m-d', strtotime("$first +1month -1day")); + + $formatDow = $this->getDowFormat(); + $end = $this->getLastDayOfWeek($last); + $day = $this->getFirstDayOfWeek($first); + $formerWeekOfTheYear = 0; + $weeks = []; + while ($day <= $end) { + $weekOfTheYear = $this->getWeekOfTheYear($day); + if ($weekOfTheYear !== $formerWeekOfTheYear) { + $weeks[$weekOfTheYear] = []; + $week = & $weeks[$weekOfTheYear]; + } + + $weekday = (int) date($formatDow, strtotime($day)); + $week[$weekday] = $day; + $day = date('Y-m-d', strtotime("$day +1day")); + $formerWeekOfTheYear = $weekOfTheYear; + } + + return $weeks; + } + + protected static function expressionToDate($expression) + { + return date('Y-m-d', strtotime($expression)); + } + + /** + * @param string $day + * @param int $increment days to add + * @return string + */ + protected static function incDay($day, $increment = 1) + { + return sprintf('%s +%dday', $day, $increment); + } + + protected static function decDay($day, $decrement = 1) + { + return sprintf('%s -%dday', $day, $decrement); + } +} diff --git a/vendor/gipfl/calendar/src/Widget/CalendarMonth.php b/vendor/gipfl/calendar/src/Widget/CalendarMonth.php new file mode 100644 index 0000000..2fe1c5b --- /dev/null +++ b/vendor/gipfl/calendar/src/Widget/CalendarMonth.php @@ -0,0 +1,177 @@ +<?php + +namespace gipfl\Calendar\Widget; + +use gipfl\Calendar\Calendar; +use gipfl\Format\LocalTimeFormat; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\Translation\TranslationHelper; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlElement; + +/** + * WARNING: API will change + */ +class CalendarMonth extends BaseHtmlElement +{ + use TranslationHelper; + + protected $tag = 'div'; + + protected $defaultAttributes = [ + 'id' => 'calendar-wrap' + ]; + + /** @var Calendar */ + protected $calendar; + + /** @var int */ + protected $now; + + /** @var Url */ + protected $url; + + /** @var HtmlElement */ + protected $days = []; + + protected $timeFormatter; + + public function __construct(Calendar $calendar, Url $url, $now) + { + $this->now = $now; + $this->url = $url; + $this->calendar = $calendar; + $this->timeFormatter = new LocalTimeFormat(); + } + + protected function dayRow() + { + return Html::tag('ul', ['class' => 'days']); + } + + /** + * @param $day + * @return HtmlElement + */ + protected function getDay($day) + { + $this->ensureAssembled(); + return $this->days[$day]; + } + + protected function hasDay($day) + { + $this->ensureAssembled(); + + return isset($this->days[$day]); + } + + protected function createDay($day) + { + $title = (int) substr($day, -2); + + if ($title === 1) { + $title = sprintf( + '%d %s', + $title, + $this->timeFormatter->getShortMonthName(strtotime($day)) + ); + } + $li = Html::tag( + 'li', + ['class' => 'day'], + Html::tag('div', ['class' => 'date'], $title) + ); + + $this->days[$day] = $li; + + return $li; + } + + public function addEvent($time, $text) + { + $day = date('Y-m-d', $time); + if (! $this->hasDay($day)) { + return $this; + } + // $this->getDay($day)->add(Html::tag('div', ['class' => 'event'], [ + $this->getDay($day)->add(Html::tag('a', ['class' => 'event', 'href' => '#'], [ + Html::tag('div', [ + 'class' => 'event-time', + 'title' => date('Y-m-d H:i:s') + ], date('H:i', $time)), + Html::tag('div', ['class' => 'event-desc'], $text) + ])); + + return $this; + } + + protected function getFormerMonth() + { + $first = date('Y-m-01', $this->now); + + return date('Y-m-d', strtotime("$first -1month")); + } + + protected function getNextMonth() + { + $first = date('Y-m-01', $this->now); + + return date('Y-m-d', strtotime("$first +1month")); + } + + protected function getNavigationLinks() + { + return Html::tag('div', ['class' => 'calendar-navigation'], [ + Link::create('<', $this->url->with('day', $this->getFormerMonth())), + Link::create('>', $this->url->with('day', $this->getNextMonth())), + ]); + } + + protected function assemble() + { + $now = $this->now; + $today = date('Y-m-d', $now); + + $this->add( + Html::tag('header', [ + $this->getNavigationLinks(), + Html::tag('h1', date('F Y', $now)) + ]) + ); + + $calendar = Html::tag('div', ['class' => 'calendar']); + $calendar->add($this->weekdaysHeader()); + $thisMonth = substr($today, 0, 7); + + foreach ($this->calendar->getWeeksForMonth($now) as $cw => $week) { + $weekRow = $this->dayRow(); + $weekRow->add( + Html::tag('li', [ + 'class' => 'weekName' + ], Html::tag('span', sprintf($this->translate('Week %s'), $cw))) + ); + foreach ($week as $day) { + $weekRow->add($this->createDay($day)); + if (substr($day, 0, 7) !== $thisMonth) { + $this->getDay($day)->addAttributes(['class' => 'other-month']); + } + } + $calendar->add($weekRow); + } + + $this->add($calendar); + } + + protected function weekdaysHeader() + { + $ul = Html::tag('ul', ['class' => 'weekdays']); + foreach ($this->calendar->listWeekDayNames() as $weekday) { + $ul->add(Html::tag('li', $this->translate($weekday))); + } + + return $ul; + } +} diff --git a/vendor/gipfl/calendar/src/Widget/CalendarMonthSummary.php b/vendor/gipfl/calendar/src/Widget/CalendarMonthSummary.php new file mode 100644 index 0000000..950530f --- /dev/null +++ b/vendor/gipfl/calendar/src/Widget/CalendarMonthSummary.php @@ -0,0 +1,278 @@ +<?php + +namespace gipfl\Calendar\Widget; + +use gipfl\Calendar\Calendar; +use gipfl\Format\LocalTimeFormat; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\Translation\TranslationHelper; +use ipl\Html\HtmlElement; +use ipl\Html\Table; + +class CalendarMonthSummary extends Table +{ + use TranslationHelper; + + protected $defaultAttributes = [ + 'data-base-target' => '_next', + 'class' => 'calendar', + ]; + + protected $today; + + protected $year; + + protected $month; + + protected $strMonth; + + protected $strToday; + + protected $days = []; + + /** @var Calendar|null */ + protected $calendar; + + protected $showWeekNumbers = true; + + protected $showOtherMonth = false; + + protected $showGrayFuture = true; + + protected $title; + + protected $color = '255, 128, 0'; + + protected $forcedMax; + + protected $timeFormat; + + public function __construct($year, $month) + { + $this->year = $year; + $this->month = $month; + $this->strMonth = sprintf('%d-%02d', $year, $month); + $this->strToday = date('Y-m-d'); + $this->timeFormat = new LocalTimeFormat(); + } + + public function setBaseColorRgb($red, $green, $blue) + { + $this->color = sprintf('%d, %d, %d', $red, $green, $blue); + + return $this; + } + + public function setCalendar(Calendar $calendar) + { + $this->calendar = $calendar; + + return $this; + } + + public function getCalendar() + { + if ($this->calendar === null) { + $this->calendar = new Calendar(); + } + + return $this->calendar; + } + + public function addEvents($events, Url $baseUrl) + { + if (empty($events)) { + return $this; + } + + if ($this->forcedMax === null) { + $max = max($events); + } else { + $max = $this->forcedMax; + } + + if ($max === 0 || $max === null) { + return $this; + } + + foreach ($events as $day => $count) { + if (! $this->hasDay($day)) { + continue; + } + + if (! $this->showOtherMonth && $this->dayIsInThisMonth($day)) { + continue; + } + + $text = (int) substr($day, -2); + + $link = Link::create($text, $baseUrl->with('day', $day)); + $alpha = $count / $max; + + if ($alpha > 0.4) { + $link->addAttributes(['style' => 'color: white;']); + } + $link->addAttributes([ + 'title' => sprintf('%d events', $count), + 'style' => sprintf( + 'background-color: rgba(%s, %.2F);', + $this->color, + $alpha + ) + ]); + + $this->getDay($day)->setContent($link); + } + + return $this; + } + + public function markNow($now = null) + { + if ($now === null) { + $now = time(); + } + $this->today = date('Y-m-d', $now); + + return $this; + } + + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + protected function getTitle() + { + if ($this->title === null) { + $this->title = $this->getMonthName() . ' ' . $this->year; + } + + return $this->title; + } + + public function forceMax($max) + { + $this->forcedMax = $max; + + return $this; + } + + protected function getMonthAsTimestamp() + { + return strtotime($this->strMonth . '-01'); + } + + protected function assemble() + { + $this->setCaption($this->getTitle()); + $this->getHeader()->add($this->createWeekdayHeader()); + $calendar = $this->getCalendar(); + foreach ($calendar->getWeeksForMonth($this->getMonthAsTimestamp()) as $cw => $week) { + $weekRow = $this->weekRow($cw); + foreach ($week as $wDay => $day) { + $dayElement = $this->createDay($day); + $otherMonth = $this->dayIsInThisMonth($day); + if ($wDay < 1 || $wDay > 5) { + $dayElement->addAttributes(['class' => 'weekend']); + } + $weekRow->add($dayElement); + } + $this->add($weekRow); + } + } + + /** + * @param $day + * @return HtmlElement + */ + protected function getDay($day) + { + $this->ensureAssembled(); + + return $this->days[$day]; + } + + protected function hasDay($day) + { + $this->ensureAssembled(); + + return isset($this->days[$day]); + } + + protected function dayIsInThisMonth($day) + { + return substr($day, 0, 7) !== $this->strMonth; + } + + protected function createDay($day) + { + $otherMonth = $this->dayIsInThisMonth($day); + $title = (int) substr($day, -2); + if ($otherMonth && ! $this->showOtherMonth) { + $title = ''; + } + $td = Table::td($title); + $this->days[$day] = $td; + + if ($otherMonth) { + $td->addAttributes(['class' => 'other-month']); + } elseif ($this->showGrayFuture && $day > $this->strToday) { + $td->addAttributes(['class' => 'future-day']); + } + + // TODO: today VS strToday?! + if ($day === $this->today) { + $td->addAttributes(['class' => 'today']); + } + + return $td; + } + + protected function weekRow($cw) + { + $row = Table::tr(); + + if ($this->showWeekNumbers) { + $row->add(Table::th(sprintf('%02d', $cw), [ + 'title' => sprintf($this->translate('Calendar Week %d'), $cw) + ])); + } + + return $row; + } + + protected function getMonthName() + { + return $this->timeFormat->getMonthName($this->getMonthAsTimestamp()); + } + + protected function createWeekdayHeader() + { + $calendar = $this->getCalendar(); + $cols = $calendar->listShortWeekDayNames(); + $row = Table::tr(); + if ($this->showWeekNumbers) { + $row->add(Table::th('')); + } + if ($calendar->firstOfWeekIsMonday()) { + $weekend = [6 => true, 7 => true]; + } else { + $weekend = [1 => true, 7 => true]; + } + $wDay = 0; + foreach ($cols as $day) { + $wDay++; + $col = Table::th($day); + if (isset($weekend[$wDay])) { + $col->addAttributes(['class' => 'weekend']); + } + $row->add($col); + } + + return $row; + } +} diff --git a/vendor/gipfl/cli/composer.json b/vendor/gipfl/cli/composer.json new file mode 100644 index 0000000..370beed --- /dev/null +++ b/vendor/gipfl/cli/composer.json @@ -0,0 +1,31 @@ +{ + "name": "gipfl/cli", + "description": "CLI utilities", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Cli\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "ext-mbstring": "*", + "ext-pcntl": "*", + "ext-posix": "*", + "react/stream": ">=1.1", + "react/promise": "^2" + }, + "require-dev": { + "react/child-process": ">=0.6" + } +} diff --git a/vendor/gipfl/cli/src/AnsiScreen.php b/vendor/gipfl/cli/src/AnsiScreen.php new file mode 100644 index 0000000..2ae3f40 --- /dev/null +++ b/vendor/gipfl/cli/src/AnsiScreen.php @@ -0,0 +1,128 @@ +<?php + +namespace gipfl\Cli; + +use InvalidArgumentException; + +/** + * Screen implementation for screens with ANSI escape code support + * + * @see http://en.wikipedia.org/wiki/ANSI_escape_code + */ +class AnsiScreen extends Screen +{ + const FG_COLORS = [ + 'black' => '30', + 'darkgray' => '1;30', + 'red' => '31', + 'lightred' => '1;31', + 'green' => '32', + 'lightgreen' => '1;32', + 'brown' => '33', + 'yellow' => '1;33', + 'blue' => '34', + 'lightblue' => '1;34', + 'purple' => '35', + 'lightpurple' => '1;35', + 'cyan' => '36', + 'lightcyan' => '1;36', + 'lightgray' => '37', + 'white' => '1;37', + ]; + + const BG_COLORS = [ + 'black' => '40', + 'red' => '41', + 'green' => '42', + 'brown' => '43', + 'blue' => '44', + 'purple' => '45', + 'cyan' => '46', + 'lightgray' => '47', + ]; + + /** + * Remove all ANSI escape codes from a given string + * @param $string + * @return string|string[]|null + */ + public function stripAnsiCodes($string) + { + return \preg_replace('/\e\[?.*?[@-~]/', '', $string); + } + + public function clear() + { + return "\033[2J" // Clear the whole screen + . "\033[1;1H" // Move the cursor to row 1, column 1 + . "\033[1S"; // Scroll whole page up by 1 line (why?) + } + + public function colorize($text, $fgColor = null, $bgColor = null) + { + return $this->startColor($fgColor, $bgColor) + . $text + . "\033[0m"; // Reset color codes + } + + public function strlen($string) + { + return parent::strlen($this->stripAnsiCodes($string)); + } + + public function underline($text) + { + return "\033[4m" + . $text + . "\033[0m"; // Reset color codes + } + + protected function fgColor($color) + { + if (! \array_key_exists($color, static::FG_COLORS)) { + throw new InvalidArgumentException( + "There is no such foreground color: $color" + ); + } + + return static::FG_COLORS[$color]; + } + + protected function bgColor($color) + { + if (! \array_key_exists($color, static::BG_COLORS)) { + throw new InvalidArgumentException( + "There is no such background color: $color" + ); + } + + return static::BG_COLORS[$color]; + } + + protected function startColor($fgColor = null, $bgColor = null) + { + $parts = []; + if ($fgColor !== null + && $bgColor !== null + && ! \array_key_exists($bgColor, static::BG_COLORS) + && \array_key_exists($bgColor, static::FG_COLORS) + && \array_key_exists($fgColor, static::BG_COLORS) + ) { + $parts[] = '7'; // reverse video, negative image + $parts[] = $this->bgColor($fgColor); + $parts[] = $this->fgColor($bgColor); + } else { + if ($fgColor !== null) { + $parts[] = $this->fgColor($fgColor); + } + if ($bgColor !== null) { + $parts[] = $this->bgColor($bgColor); + } + } + if (empty($parts)) { + return ''; + } + + return "\033[" . \implode(';', $parts) . 'm'; + } +} diff --git a/vendor/gipfl/cli/src/Process.php b/vendor/gipfl/cli/src/Process.php new file mode 100644 index 0000000..45c67b5 --- /dev/null +++ b/vendor/gipfl/cli/src/Process.php @@ -0,0 +1,141 @@ +<?php + +namespace gipfl\Cli; + +class Process +{ + /** @var string|null */ + protected static $initialCwd; + + /** + * Set the command/process title for this process + * + * @param $title + */ + public static function setTitle($title) + { + if (function_exists('cli_set_process_title')) { + \cli_set_process_title($title); + } + } + + /** + * Replace this process with a new instance of itself by executing the + * very same binary with the very same parameters + */ + public static function restart() + { + // _ is only available when executed via shell + $binary = static::getEnv('_'); + $argv = $_SERVER['argv']; + if (\strlen($binary) === 0) { + // Problem: this doesn't work if we changed working directory and + // called the binary with a relative path. Something that doesn't + // happen when started as a daemon, and when started manually we + // should have $_ from our shell. + $binary = static::absoluteFilename(\array_shift($argv)); + } else { + \array_shift($argv); + } + \pcntl_exec($binary, $argv, static::getEnv()); + } + + /** + * Get the given ENV variable, null if not available + * + * Returns an array with all ENV variables if no $key is given + * + * @param string|null $key + * @return array|string|null + */ + public static function getEnv($key = null) + { + if ($key !== null) { + return \getenv($key); + } + + if (PHP_VERSION_ID > 70100) { + return \getenv(); + } else { + $env = $_SERVER; + unset($env['argv'], $env['argc']); + + return $env; + } + } + + /** + * Get the path to the executed binary when starting this command + * + * This fails if we changed working directory and called the binary with a + * relative path. Something that doesn't happen when started as a daemon. + * When started manually we should have $_ from our shell. + * + * To be always on the safe side please call Process::getInitialCwd() once + * after starting your process and before switching directory. That way we + * preserve our initial working directory. + * + * @return mixed|string + */ + public static function getBinaryPath() + { + if (isset($_SERVER['_'])) { + return $_SERVER['_']; + } else { + global $argv; + + return static::absoluteFilename($argv[0]); + } + } + + /** + * The working directory as given by getcwd() the very first time we + * called this method + * + * @return string + */ + public static function getInitialCwd() + { + if (self::$initialCwd === null) { + self::$initialCwd = \getcwd(); + } + + return self::$initialCwd; + } + + /** + * Returns the absolute filename for the given file + * + * If relative, it's calculated in relation to the given working directory. + * The current working directory is being used if null is given. + * + * @param $filename + * @param null $cwd + * @return string + */ + public static function absoluteFilename($filename, $cwd = null) + { + $filename = \str_replace( + DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR, + $filename + ); + if ($filename[0] === '.') { + $filename = ($cwd ?: \getcwd()) . DIRECTORY_SEPARATOR . $filename; + } + $parts = \explode(DIRECTORY_SEPARATOR, $filename); + $result = []; + foreach ($parts as $part) { + if ($part === '.') { + continue; + } + if ($part === '..') { + \array_pop($result); + continue; + } + $result[] = $part; + } + + return \implode(DIRECTORY_SEPARATOR, $result); + } +} diff --git a/vendor/gipfl/cli/src/Screen.php b/vendor/gipfl/cli/src/Screen.php new file mode 100644 index 0000000..cb05a3f --- /dev/null +++ b/vendor/gipfl/cli/src/Screen.php @@ -0,0 +1,190 @@ +<?php + +namespace gipfl\Cli; + +/** + * Base class providing minimal CLI Screen functionality. While classes + * extending this one (read: AnsiScreen) should implement all the fancy cool + * things, this base class makes sure that your code will still run in + * environments with no ANSI or similar support + * + * ```php + * $screen = Screen::instance(); + * echo $screen->center($screen->underline('Hello world')); + * ``` + */ +class Screen +{ + protected $isUtf8; + + /** + * Get a new Screen instance. + * + * For now this is limited to either a very basic Screen implementation as + * a fall-back or an AnsiScreen implementation with more functionality + * + * @return AnsiScreen|Screen + */ + public static function factory() + { + if (! defined('STDOUT')) { + return new Screen(); + } + if (\function_exists('posix_isatty') && \posix_isatty(STDOUT)) { + return new AnsiScreen(); + } else { + return new Screen(); + } + } + + /** + * Center the given string horizontally on the current screen + * + * @param $string + * @return string + */ + public function center($string) + { + $len = $this->strlen($string); + $width = (int) \floor(($this->getColumns() + $len) / 2) - $len; + + return \str_repeat(' ', $width) . $string; + } + + /** + * Clear the screen + * + * Impossible for non-ANSI screens, so let's output a newline for now + * + * @return string + */ + public function clear() + { + return "\n"; + } + + /** + * Colorize the given text. Has no effect on a basic Screen, all colors + * will be accepted. It's prefectly legal to provide background or foreground + * only + * + * Returns the very same string, eventually enriched with related ANSI codes + * + * @param $text + * @param null $fgColor + * @param null $bgColor + * + * @return mixed + */ + public function colorize($text, $fgColor = null, $bgColor = null) + { + return $text; + } + + /** + * Generate $count newline characters + * + * @param int $count + * @return string + */ + public function newlines($count = 1) + { + return \str_repeat(PHP_EOL, $count); + } + + /** + * Calculate the visible length of a given string. While this is simple on + * a non-ANSI-screen, such implementation will be required to strip control + * characters to get the correct result + * + * @param $string + * @return int + */ + public function strlen($string) + { + if ($this->isUtf8()) { + return \mb_strlen($string, 'UTF-8'); + } else { + return \strlen($string); + } + } + + /** + * Underline the given text - if possible + * + * @return string + */ + public function underline($text) + { + return $text; + } + + /** + * Get the number of currently available columns. Please note that this + * might chance at any time while your program is running + * + * @return int + */ + public function getColumns() + { + $cols = (int) \getenv('COLUMNS'); + if (! $cols) { + // stty -a ? + $cols = (int) \exec('tput cols'); + } + if (! $cols) { + $cols = 80; + } + + return $cols; + } + + /** + * Get the number of currently available rows. Please note that this + * might chance at any time while your program is running + * + * @return int + */ + public function getRows() + { + $rows = (int) \getenv('ROWS'); + if (! $rows) { + // stty -a ? + $rows = (int) \exec('tput lines'); + } + if (! $rows) { + $rows = 25; + } + + return $rows; + } + + /** + * Whether we're on a UTF-8 screen. We assume latin1 otherwise, there is no + * support for additional encodings + * + * @return bool + */ + public function isUtf8() + { + if ($this->isUtf8 === null) { + // null should equal 0 here, however seems to equal '' on some systems: + $current = \setlocale(LC_ALL, 0); + + $parts = explode(';', $current); + $lc_parts = []; + foreach ($parts as $part) { + if (\strpos($part, '=') === false) { + continue; + } + list($key, $val) = explode('=', $part, 2); + $lc_parts[$key] = $val; + } + + $this->isUtf8 = \array_key_exists('LC_CTYPE', $lc_parts) + && \preg_match('~\.UTF-8$~i', $lc_parts['LC_CTYPE']); + } + + return $this->isUtf8; + } +} diff --git a/vendor/gipfl/cli/src/Spinner.php b/vendor/gipfl/cli/src/Spinner.php new file mode 100644 index 0000000..b949526 --- /dev/null +++ b/vendor/gipfl/cli/src/Spinner.php @@ -0,0 +1,69 @@ +<?php + +namespace gipfl\Cli; + +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise\ExtendedPromiseInterface; + +class Spinner +{ + const ASCII_SLASH = ['/', '-', '\\', '|']; + const ASCII_BOUNCING_CIRCLE = ['.', 'o', 'O', '°', 'O', 'o']; + const ROTATING_HALF_CIRCLE = ['◑', '◒', '◐', '◓']; + const ROTATING_EARTH = ['🌎', '🌏', '🌍']; + const ROTATING_MOON = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘']; + const UP_DOWN_BAR = [' ', '_', '▁', '▃', '▄', '▅', '▆', '▇', '▆', '▅', '▄', '▃', '▁']; + const CLOCK = ['🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛']; + const WAVING_DOTS = ['⢄', '⢂', '⢁', '⡁', '⡈', '⡐', '⡠', '⡐', '⡈', '⡁', '⢁', '⢂']; + const ROTATING_DOTS = ['⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽', '⣾']; + + /** @var LoopInterface */ + protected $loop; + + protected $frames; + + protected $frame = -1; + + protected $count; + + protected $delay; + + public function __construct(LoopInterface $loop, array $frames = self::ASCII_SLASH) + { + $this->loop = $loop; + $this->frames = $frames; + $this->count = \count($frames); + $this->delay = ((int) (2 * 100 / $this->count)) / 100; + } + + protected function getNextFrame() + { + $first = $this->frame === -1; + $this->frame++; + if ($this->frame >= $this->count) { + $this->frame = 0; + } + + return $this->frames[$this->frame]; + } + + public function spinWhile(ExtendedPromiseInterface $promise, callable $renderer) + { + $next = function () use ($renderer) { + $renderer($this->getNextFrame()); + }; + $spinTimer = $this->loop->addPeriodicTimer($this->delay, $next); + $deferred = new Deferred(function () use ($spinTimer) { + $this->loop->cancelTimer($spinTimer); + }); + $this->loop->futureTick($next); + $wait = $deferred->promise(); + $cancel = function () use ($wait) { + $wait->cancel(); + }; + $promise->otherwise($cancel)->then($cancel); + + return $promise; + } +} diff --git a/vendor/gipfl/cli/src/Tty.php b/vendor/gipfl/cli/src/Tty.php new file mode 100644 index 0000000..efe5924 --- /dev/null +++ b/vendor/gipfl/cli/src/Tty.php @@ -0,0 +1,132 @@ +<?php + +namespace gipfl\Cli; + +use InvalidArgumentException; +use React\EventLoop\LoopInterface; +use React\Stream\ReadableResourceStream; +use React\Stream\WritableResourceStream; +use RuntimeException; +use function defined; +use function fstat; +use function function_exists; +use function is_bool; +use function is_resource; +use function is_string; +use function posix_isatty; +use function register_shutdown_function; +use function stream_isatty; +use function stream_set_blocking; +use function strlen; +use function var_export; + +class Tty +{ + protected $stdin; + + protected $stdout; + + protected $loop; + + protected $echo = true; + + /** @var TtyMode */ + protected $ttyMode; + + public function __construct(LoopInterface $loop) + { + $this->loop = $loop; + register_shutdown_function([$this, 'restore']); + $loop->futureTick(function () { + $this->initialize(); + }); + } + + public function setEcho($echo) + { + if (! is_bool($echo) && ! is_string($echo) && strlen($echo) !== 1) { + throw new InvalidArgumentException( + "\$echo must be boolean or a single character, got " . var_export($echo, 1) + ); + } + $this->echo = $echo; + if ($this->ttyMode) { + if ($echo) { + $this->ttyMode->enableFeature('echo'); + } else { + $this->ttyMode->disableFeature('echo'); + } + } + + return $this; + } + + public function stdin() + { + if ($this->stdin === null) { + $this->assertValidStdin(); + $this->stdin = new ReadableResourceStream(STDIN, $this->loop); + } + + return $this->stdin; + } + + protected function hasStdin() + { + return defined('STDIN') && is_resource(STDIN) && fstat(STDIN) !== false; + } + + protected function assertValidStdin() + { + if (! $this->hasStdin()) { + throw new RuntimeException('I have no STDIN'); + } + } + + public function stdout() + { + if ($this->stdout === null) { + $this->assertValidStdout(); + $this->stdout = new WritableResourceStream(STDOUT, $this->loop); + } + + return $this->stdout; + } + + protected function hasStdout() + { + return defined('STDOUT') && is_resource(STDOUT) && fstat(STDOUT) !== false; + } + + protected function assertValidStdout() + { + if (! $this->hasStdout()) { + throw new RuntimeException('I have no STDOUT'); + } + } + + protected function initialize() + { + $this->ttyMode = new TtyMode(); + $this->ttyMode->setPreferredMode($this->echo); + } + + public static function isSupported() + { + if (PHP_VERSION_ID >= 70200) { + return stream_isatty(STDIN); + } elseif (function_exists('posix_isatty')) { + return posix_isatty(STDIN); + } else { + return false; + } + } + + public function restore() + { + if ($this->hasStdin()) { + // ReadableResourceStream sets blocking to false, let's restore this + stream_set_blocking(STDIN, true); + } + } +} diff --git a/vendor/gipfl/cli/src/TtyMode.php b/vendor/gipfl/cli/src/TtyMode.php new file mode 100644 index 0000000..8b9c884 --- /dev/null +++ b/vendor/gipfl/cli/src/TtyMode.php @@ -0,0 +1,95 @@ +<?php + +namespace gipfl\Cli; + +use function escapeshellarg; +use function register_shutdown_function; +use function rtrim; +use function shell_exec; + +class TtyMode +{ + protected $originalMode; + + public function enableCanonicalMode() + { + $this->enableFeature('icanon'); + + return $this; + } + + public function disableCanonicalMode() + { + $this->disableFeature('icanon'); + + return $this; + } + + public function enableFeature(...$feature) + { + $this->preserve(); + $cmd = 'stty '; + foreach ($feature as $f) { + $cmd .= escapeshellarg($f); + } + + shell_exec($cmd); + } + + public function disableFeature(...$feature) + { + $this->preserve(); + $cmd = 'stty'; + foreach ($feature as $f) { + $cmd .= ' -' . escapeshellarg($f); + } + + shell_exec($cmd); + } + + public function getCurrentMode() + { + return rtrim(shell_exec('stty -g'), PHP_EOL); + } + + /** + * Helper allowing to call stty only once for the mose used flags, icanon and echo + * @param bool $echo + * @return $this + */ + public function setPreferredMode($echo = true) + { + $this->preserve(); + if ($echo) { + $this->disableFeature('icanon'); + } else { + $this->disableFeature('icanon', 'echo'); + } + + return $this; + } + + /** + * @internal + */ + public function preserve($force = false) + { + if ($force || $this->originalMode === null) { + $this->originalMode = $this->getCurrentMode(); + register_shutdown_function([$this, 'restore']); + } + + return $this; + } + + /** + * @internal + */ + public function restore() + { + if ($this->originalMode) { + shell_exec('stty ' . escapeshellarg($this->originalMode)); + $this->originalMode = null; + } + } +} diff --git a/vendor/gipfl/curl/LICENSE b/vendor/gipfl/curl/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/curl/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/curl/composer.json b/vendor/gipfl/curl/composer.json new file mode 100644 index 0000000..845793d --- /dev/null +++ b/vendor/gipfl/curl/composer.json @@ -0,0 +1,29 @@ +{ + "name": "gipfl/curl", + "description": "ReactPHP-friendly async CURL abstraction", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Curl\\": "src" + } + }, + "require": { + "php": ">=5.6.3", + "ext-curl": "*", + "guzzlehttp/psr7": ">=1.6", + "psr/http-message": "^1.0", + "react/event-loop": ">=1.0", + "react/promise": ">=2", + "react/stream": ">=1.0" + } +} diff --git a/vendor/gipfl/curl/src/CurlAsync.php b/vendor/gipfl/curl/src/CurlAsync.php new file mode 100644 index 0000000..f9bd5d0 --- /dev/null +++ b/vendor/gipfl/curl/src/CurlAsync.php @@ -0,0 +1,338 @@ +<?php + +namespace gipfl\Curl; + +use Exception; +use GuzzleHttp\Psr7\Message; +use GuzzleHttp\Psr7\Request; +use Psr\Http\Message\RequestInterface; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; +use React\Promise\Deferred; +use RuntimeException; +use Throwable; +use function array_shift; +use function count; +use function curl_close; +use function curl_error; +use function curl_multi_add_handle; +use function curl_multi_close; +use function curl_multi_exec; +use function curl_multi_getcontent; +use function curl_multi_info_read; +use function curl_multi_init; +use function curl_multi_remove_handle; +use function curl_multi_select; +use function curl_multi_setopt; +use function curl_multi_strerror; + +/** + * This class provides an async CURL abstraction layer fitting into a ReactPHP + * reality, implemented based on curl_multi. + * + * As long as there are requests pending, a timer fires + * + */ +class CurlAsync +{ + const DEFAULT_POLLING_INTERVAL = 0.03; + + /** @var false|resource */ + protected $handle; + + /** @var Deferred[] resourceIdx => Deferred */ + protected $running = []; + + /** @var [ [0 => resourceIdx, 1 => Deferred], ... ] */ + protected $pending = []; + + /** @var array[] resourceIdx => options */ + protected $pendingOptions = []; + + /** @var RequestInterface[] resourceIdx => RequestInterface */ + protected $pendingRequests = []; + + /** @var array resourceIdx => resource */ + protected $curl = []; + + /** @var int */ + protected $maxParallelRequests = 30; + + /** @var LoopInterface */ + protected $loop; + + /** @var float */ + protected $fastInterval = self::DEFAULT_POLLING_INTERVAL; + + /** @var TimerInterface */ + protected $fastTimer; + + /** + * @param LoopInterface $loop + */ + public function __construct(LoopInterface $loop) + { + $this->loop = $loop; + $this->handle = curl_multi_init(); + // Hint: I had no specific reason to disable pipelining, nothing but + // the desire to ease debugging. So, in case you feel confident you might + // want to remove this line + curl_multi_setopt($this->handle, CURLMOPT_PIPELINING, 0); + if (! $this->handle) { + throw new RuntimeException('Failed to initialize curl_multi'); + } + } + + public function get($url, $headers = [], $curlOptions = []) + { + return $this->send(new Request('GET', $url, $headers), $curlOptions); + } + + public function post($url, $headers = [], $body = null, $curlOptions = []) + { + return $this->send(new Request('POST', $url, $headers, $body), $curlOptions); + } + + public function head($url, $headers = []) + { + return $this->send(new Request('HEAD', $url, $headers)); + } + + public function send(RequestInterface $request, $curlOptions = []) + { + $curl = CurlHandle::createForRequest($request, $curlOptions); + $idx = (int) $curl; + $this->curl[$idx] = $curl; + $this->pendingOptions[$idx] = $curlOptions; + $this->pendingRequests[$idx] = $request; + $deferred = new Deferred(function () use ($idx) { + $this->freeByResourceReference($idx); + }); + $this->pending[] = [$idx, $deferred]; + $this->loop->futureTick(function () { + $this->enablePolling(); + $this->enqueueNextRequestIfAny(); + }); + + return $deferred->promise(); + } + + /** + * @param int $max + * @return $this + */ + public function setMaxParallelRequests($max) + { + $this->maxParallelRequests = (int) $max; + + return $this; + } + + public function getPendingCurlHandles() + { + return $this->curl; + } + + protected function enqueueNextRequestIfAny() + { + while (count($this->pending) > 0 && count($this->running) < $this->maxParallelRequests) { + $next = array_shift($this->pending); + $resourceIdx = $next[0]; + $this->running[$resourceIdx] = $next[1]; + $curl = $this->curl[$resourceIdx]; + // enqueued: curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + curl_multi_add_handle($this->handle, $curl); + } + } + + public function rejectAllPendingRequests($reasonOrError = null) + { + $this->rejectAllRunningRequests($reasonOrError); + } + + protected function rejectAllRunningRequests($reasonOrError = null) + { + $running = $this->running; // Hint: intentionally cloned + foreach ($running as $resourceNum => $deferred) { + $this->freeByResourceReference($resourceNum); + $deferred->reject($reasonOrError); + } + if (! empty($this->running)) { + throw new RuntimeException( + // Hint: should never be reached + 'All running requests should have been removed, but something has been left' + ); + } + } + + protected function rejectAllDeferredRequests($reasonOrError = null) + { + foreach ($this->pending as $pending) { + list($resourceNum, $deferred) = $pending; + $this->freeByResourceReference($resourceNum); + $deferred->reject($reasonOrError); + } + + if (! empty($this->running)) { + throw new RuntimeException( + // Hint: should never be reached + 'All pending requests should have been removed, but something has been left' + ); + } + } + + /** + * Returns true in case at least one request completed + * + * @return bool + */ + protected function checkForResults() + { + if (empty($this->running)) { + return false; + } else { + $handle = $this->handle; + do { + $status = curl_multi_exec($handle, $active); + } while ($status > 0); + // Hint: while ($status === CURLM_CALL_MULTI_PERFORM) ? + + if ($status !== CURLM_OK) { + throw new RuntimeException(curl_multi_strerror($handle)); + } + if ($active) { + $fds = curl_multi_select($handle, 0.01); + // We take no action here, we'll info_read anyways: + // $fds === -1 -> select failed, returning. Probably only happens when running out of FDs + // $fds === 0 -> Nothing to do + // TODO: figure how often we get here -> https://bugs.php.net/bug.php?id=61141 + } + $gotResult = false; + while (false !== ($completed = curl_multi_info_read($handle))) { + $this->requestCompleted($handle, $completed); + if (empty($this->pending) && empty($this->running)) { + $this->disablePolling(); + } + $gotResult = true; + } + + return $gotResult; + } + } + + protected function requestCompleted($handle, $completed) + { + $curl = $completed['handle']; + $resourceNum = (int) $curl; // Hint this is an object in PHP >= 8, a resource in older versions + $deferred = $this->running[$resourceNum]; + $request = $this->pendingRequests[$resourceNum]; + $options = $this->pendingOptions[$resourceNum]; + $content = curl_multi_getcontent($curl); + curl_multi_remove_handle($handle, $curl); + $removeProxyHeaders = isset($options[CURLOPT_PROXYTYPE]) + && $options[CURLOPT_PROXYTYPE] === CURLPROXY_HTTP + // We assume that CURLOPT_SUPPRESS_CONNECT_HEADERS has been set for the request + && !defined('CURLOPT_SUPPRESS_CONNECT_HEADERS'); + + if ($completed['result'] === CURLE_OK) { + $this->freeByResourceReference($resourceNum); + try { + $deferred->resolve($this->parseResponse($content, $removeProxyHeaders)); + } catch (\Exception $e) { + $deferred->reject(new ResponseParseError($e->getMessage(), $request, null, $e->getCode(), $e)); + } + } else { + try { + $deferred->resolve($this->parseResponse($content, $removeProxyHeaders)); + } catch (\Exception $e) { + $response = null; + } + try { + $error = curl_error($curl); + if ($error === '') { + $error = 'Curl failed, but got no CURL error'; + } + } catch (Throwable $e) { + $error = 'Unable to determine CURL error: ' . $e->getMessage(); + } catch (Exception $e) { + $error = 'Unable to determine CURL error: ' . $e->getMessage(); + } + $deferred->reject(new RequestError($error, $request, $response)); + $this->freeByResourceReference($resourceNum); + } + } + + protected function parseResponse($content, $stripProxyHeaders) + { + // This method can be removed once we support PHP 7.3+ only, as it + // has CURLOPT_SUPPRESS_CONNECT_HEADERS + $response = Message::parseResponse($content); + if ($stripProxyHeaders) { + $body = (string) $response->getBody(); + if (preg_match('/^HTTP\/.*? [0-9]{3}[^\n]*\r?\n/s', $body)) { + // There is no such header on reused connections + $response = Message::parseResponse($body); + } + } + + return $response; + } + + /** + * Set the polling interval used while requests are pending. Defaults to + * self::DEFAULT_POLLING_INTERVAL + * + * @param float $interval + */ + public function setInterval($interval) + { + if ($interval !== $this->fastInterval) { + $this->fastInterval = $interval; + $this->reEnableTimerIfActive(); + } + } + + protected function reEnableTimerIfActive() + { + if ($this->fastTimer !== null) { + $this->disablePolling(); + $this->enablePolling(); + } + } + + /** + * Polling timer should be active only while requests are pending + */ + protected function enablePolling() + { + if ($this->fastTimer === null) { + $this->fastTimer = $this->loop->addPeriodicTimer($this->fastInterval, function () { + if ($this->checkForResults()) { + $this->enqueueNextRequestIfAny(); + } + }); + } + } + + protected function disablePolling() + { + if ($this->fastTimer) { + $this->loop->cancelTimer($this->fastTimer); + $this->fastTimer = null; + } + } + + protected function freeByResourceReference($ref) + { + unset($this->pendingRequests[$ref]); + unset($this->pendingOptions[$ref]); + unset($this->running[$ref]); + curl_close($this->curl[$ref]); + unset($this->curl[$ref]); + } + + public function __destruct() + { + curl_multi_close($this->handle); + } +} diff --git a/vendor/gipfl/curl/src/CurlHandle.php b/vendor/gipfl/curl/src/CurlHandle.php new file mode 100644 index 0000000..d5309f0 --- /dev/null +++ b/vendor/gipfl/curl/src/CurlHandle.php @@ -0,0 +1,76 @@ +<?php + +namespace gipfl\Curl; + +use Psr\Http\Message\RequestInterface; + +class CurlHandle +{ + protected static $curlOptions = [ + CURLOPT_HEADER => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_ENCODING => 'gzip', + CURLOPT_TCP_NODELAY => true, + CURLINFO_HEADER_OUT => true, + CURLOPT_TCP_KEEPALIVE => 1, + CURLOPT_BUFFERSIZE => 512 * 1024, + ]; + + public static function createForRequest(RequestInterface $request, $curlOptions = []) + { + $headers = []; + foreach ($request->getHeaders() as $name => $values) { + foreach ($values as $value) { + $headers[] = "$name: $value"; + } + } + $body = $request->getBody(); + if ($body->getSize() > 0) { + $body = $body->getContents(); + } else { + $body = null; + } + + + $curl = curl_init(); + $opts = static::prepareCurlOptions( + $request->getMethod(), + (string) $request->getUri(), + $body, + $headers, + $curlOptions + ); + curl_setopt_array($curl, $opts); + + return $curl; + } + + protected static function prepareCurlOptions($method, $url, $body = null, $headers = [], $curlOptions = []) + { + $opts = $curlOptions + [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_URL => $url, + ] + self::$curlOptions; + + if (isset($opts[CURLOPT_HTTPHEADER])) { + $opts[CURLOPT_HTTPHEADER] = array_merge($opts[CURLOPT_HTTPHEADER], $headers); + } else { + $opts[CURLOPT_HTTPHEADER] = $headers; + } + if (isset($opts[CURLOPT_PROXYTYPE]) + && $opts[CURLOPT_PROXYTYPE] === CURLPROXY_HTTP + && defined('CURLOPT_SUPPRESS_CONNECT_HEADERS') + ) { + $opts[CURLOPT_SUPPRESS_CONNECT_HEADERS] = true; + } + + if ($body !== null) { + $opts[CURLOPT_POSTFIELDS] = $body; + } + + return $opts; + } +} diff --git a/vendor/gipfl/curl/src/RequestError.php b/vendor/gipfl/curl/src/RequestError.php new file mode 100644 index 0000000..a65df3a --- /dev/null +++ b/vendor/gipfl/curl/src/RequestError.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\Curl; + +use Exception; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +class RequestError extends Exception // implements Psr\Http\Client\RequestExceptionInterface +{ + /** @var RequestInterface */ + protected $request; + + /** @var ResponseInterface */ + protected $response; + + public function __construct( + $message, + RequestInterface $request, + ?ResponseInterface $response = null, + $code = 0, + ?Exception $previous = null + ) { + parent::__construct($message, $code, $previous); + $this->request = $request; + $this->response = $response; + } + + /** + * @return RequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return ?ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/vendor/gipfl/curl/src/ResponseParseError.php b/vendor/gipfl/curl/src/ResponseParseError.php new file mode 100644 index 0000000..5f127b6 --- /dev/null +++ b/vendor/gipfl/curl/src/ResponseParseError.php @@ -0,0 +1,7 @@ +<?php + +namespace gipfl\Curl; + +class ResponseParseError extends RequestError +{ +} diff --git a/vendor/gipfl/data-type/composer.json b/vendor/gipfl/data-type/composer.json new file mode 100644 index 0000000..412c294 --- /dev/null +++ b/vendor/gipfl/data-type/composer.json @@ -0,0 +1,24 @@ +{ + "name": "gipfl/data-type", + "description": "Serializable Data Types", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\DataType\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "gipfl/json": ">=0.2.0" + } +} diff --git a/vendor/gipfl/data-type/src/SetOfSettings.php b/vendor/gipfl/data-type/src/SetOfSettings.php new file mode 100644 index 0000000..731a2d7 --- /dev/null +++ b/vendor/gipfl/data-type/src/SetOfSettings.php @@ -0,0 +1,79 @@ +<?php + +namespace gipfl\DataType; + +use gipfl\Json\JsonSerialization; +use gipfl\Json\JsonString; +use function ksort; + +class SetOfSettings implements JsonSerialization +{ + /** @var Settings[] */ + protected $sections = []; + + /** + * @param Settings[]|array|\stdClass $set + */ + public function __construct(array $set = []) + { + foreach ((array) $set as $section => $settings) { + $this->setSection($section, $settings); + } + } + + public static function fromSerialization($any) + { + return new static($any); + } + + public function set($section, $setting, $value) + { + if (! isset($this->sections[$section])) { + $this->sections[$section] = new Settings(); + } + $this->sections[$section]->set($setting, $value); + + return $this; + } + + public function get($section, $setting, $default = null) + { + if (isset($this->sections[$section])) { + return $this->sections[$section]->get($setting, $default); + } + + return $default; + } + + public function setSection($section, $settings) + { + if ($settings instanceof Settings) { + $this->sections[$section] = clone($settings); + } else { + $this->sections[$section] = new Settings($settings); + } + + return $this; + } + + public function cloneSection($section) + { + if (array_key_exists($section, $this->sections)) { + return clone($this->sections[$section]); + } + + return new Settings(); + } + + public function equals(SetOfSettings $settings) + { + return JsonString::encode($settings) === JsonString::encode($this); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + ksort($this->sections); + return (object) $this->sections; + } +} diff --git a/vendor/gipfl/data-type/src/Settings.php b/vendor/gipfl/data-type/src/Settings.php new file mode 100644 index 0000000..64b179d --- /dev/null +++ b/vendor/gipfl/data-type/src/Settings.php @@ -0,0 +1,103 @@ +<?php + +namespace gipfl\DataType; + +use gipfl\Json\JsonSerialization; +use gipfl\Json\JsonString; +use gipfl\Json\SerializationHelper; +use InvalidArgumentException; +use stdClass; +use function array_key_exists; +use function ksort; + +class Settings implements JsonSerialization +{ + protected $settings = []; + + /** + * @param object|array $settings + */ + public function __construct($settings = []) + { + foreach ((array) $settings as $property => $value) { + $this->set($property, $value); + } + } + + /** + * @param stdClass|array $object + * @return static + */ + public static function fromSerialization($object) + { + return new static($object); + } + + public function set($name, $value) + { + SerializationHelper::assertSerializableValue($value); + $this->settings[$name] = $value; + } + + public function get($name, $default = null) + { + if ($this->has($name)) { + return $this->settings[$name]; + } + + return $default; + } + + public function getArray($name, $default = []) + { + if ($this->has($name)) { + return (array) $this->settings[$name]; + } + + return $default; + } + + public function requireArray($name) + { + return (array) $this->getRequired(($name)); + } + + public function getAsSettings($name, Settings $default = null) + { + if ($this->has($name)) { + return Settings::fromSerialization($this->settings[$name]); + } + + if ($default === null) { + return new Settings(); + } + + return $default; + } + + public function getRequired($name) + { + if ($this->has($name)) { + return $this->settings[$name]; + } + + throw new InvalidArgumentException("Setting '$name' is not available"); + } + + public function has($name) + { + return array_key_exists($name, $this->settings); + } + + public function equals(Settings $settings) + { + return JsonString::encode($settings) === JsonString::encode($this); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + ksort($this->settings); + return (object) $this->settings; + } +} diff --git a/vendor/gipfl/db-migration/composer.json b/vendor/gipfl/db-migration/composer.json new file mode 100644 index 0000000..da2ae4a --- /dev/null +++ b/vendor/gipfl/db-migration/composer.json @@ -0,0 +1,16 @@ +{ + "name": "gipfl/db-migration", + "description": "Simple DB migration helper", + "type": "library", + "require": { + "php": ">=5.6" + }, + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\DbMigration\\": "src" + } + } +} diff --git a/vendor/gipfl/db-migration/src/Migration.php b/vendor/gipfl/db-migration/src/Migration.php new file mode 100644 index 0000000..2e6c586 --- /dev/null +++ b/vendor/gipfl/db-migration/src/Migration.php @@ -0,0 +1,73 @@ +<?php + +namespace gipfl\DbMigration; + +use Exception; +use gipfl\ZfDb\Adapter\Pdo\PdoAdapter as Db; +use InvalidArgumentException; +use RuntimeException; +use Zend_Db_Adapter_Pdo_Abstract as ZfDb; + +class Migration +{ + /** + * @var string + */ + protected $sql; + + /** + * @var int + */ + protected $version; + + public function __construct($version, $sql) + { + $this->version = $version; + $this->sql = $sql; + } + + /** + * @param Db|ZfDb $db + * @return $this + */ + public function apply($db) + { + if (! ($db instanceof Db || $db instanceof ZfDb)) { + throw new InvalidArgumentException('$db must be an valid Zend_Db PDO adapter'); + } + // TODO: this is fragile and depends on accordingly written schema files: + $sql = preg_replace('/-- .*$/m', '', $this->sql); + $queries = preg_split( + '/[\n\s\t]*;[\n\s\t]+/s', + $sql, + -1, + PREG_SPLIT_NO_EMPTY + ); + + if (empty($queries)) { + throw new RuntimeException(sprintf( + 'Migration %d has no queries', + $this->version + )); + } + + try { + foreach ($queries as $query) { + if (preg_match('/^(?:OPTIMIZE|EXECUTE) /i', $query)) { + $db->query($query); + } else { + $db->exec($query); + } + } + } catch (Exception $e) { + throw new RuntimeException(sprintf( + 'Migration %d failed (%s) while running %s', + $this->version, + $e->getMessage(), + $query + )); + } + + return $this; + } +} diff --git a/vendor/gipfl/db-migration/src/Migrations.php b/vendor/gipfl/db-migration/src/Migrations.php new file mode 100644 index 0000000..2f85aa4 --- /dev/null +++ b/vendor/gipfl/db-migration/src/Migrations.php @@ -0,0 +1,299 @@ +<?php + +namespace gipfl\DbMigration; + +use DirectoryIterator; +use Exception; +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Adapter\Adapter as Db; +use gipfl\ZfDb\Adapter\Pdo\Mysql; +use gipfl\ZfDb\Adapter\Pdo\Pgsql; +use InvalidArgumentException; +use Zend_Db_Adapter_Pdo_Abstract as ZfDb; +use Zend_Db_Adapter_Pdo_Mysql as ZfMysql; +use Zend_Db_Adapter_Pdo_Pgsql as ZfPgsql; + +class Migrations +{ + const DB_TYPE_MYSQL = 'mysql'; + + const DB_TYPE_POSTGRESQL = 'pgsql'; + + /** @var Db */ + protected $db; + + /** @var string mysql or pgsql */ + protected $dbType; + + /** @var string */ + protected $schemaDirectory; + + /** @var string */ + protected $tableName; + + /** + * Migrations constructor. + * + * @param Db|ZfDb $db + * @param string $schemaDirectory + * @param string $tableName + */ + public function __construct($db, $schemaDirectory, $tableName = 'schema_migration') + { + if (! ($db instanceof Db || $db instanceof ZfDb)) { + throw new InvalidArgumentException('$db must be an valid Zend_Db PDO adapter'); + } + $this->db = $db; + if ($db instanceof Mysql || $db instanceof ZfMysql) { + $this->dbType = self::DB_TYPE_MYSQL; + } elseif ($db instanceof Pgsql || $db instanceof ZfPgsql) { + $this->dbType = self::DB_TYPE_POSTGRESQL; + } else { + throw new InvalidArgumentException(sprintf( + 'Migrations are currently supported for MySQL and PostgreSQL only, got %s', + get_class($db) + )); + } + $this->tableName = (string) $tableName; + $this->schemaDirectory = (string) $schemaDirectory; + } + + /** + * Still unused + * + * @throws AdapterException|\Zend_Db_Adapter_Exception + */ + protected function createMigrationsTable() + { + if ($this->dbType === self::DB_TYPE_POSTGRESQL) { + $create = /** @lang text */ + <<<SQL + +CREATE TABLE {$this->tableName} ( + schema_version SMALLINT NOT NULL, + migration_time TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (schema_version) +); + +SQL; + } else { + $create = /** @lang text */ + <<<SQL +CREATE TABLE {$this->tableName} ( + schema_version SMALLINT UNSIGNED NOT NULL, + migration_time DATETIME NOT NULL, + PRIMARY KEY (schema_version) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin; +SQL; + } + $this->db->exec($create); + } + + /** + * @return int + */ + public function getLastMigrationNumber() + { + try { + $query = $this->db->select()->from( + ['m' => $this->getTableName()], + ['schema_version' => 'MAX(schema_version)'] + ); + + return (int) $this->db->fetchOne($query); + } catch (Exception $e) { + return 0; + } + } + + /** + * @return string + */ + protected function getTableName() + { + return $this->tableName; + } + + /** + * @return bool + */ + public function hasAnyTable() + { + return count($this->db->listTables()) > 0; + } + + /** + * @return bool + */ + public function hasTable($tableName) + { + return in_array($tableName, $this->db->listTables()); + } + + /** + * @return bool + */ + public function hasMigrationsTable() + { + return $this->hasTable($this->tableName); + } + + /** + * @return bool + */ + public function hasSchema() + { + return $this->listPendingMigrations() !== [0]; + } + + /** + * @return bool + */ + public function hasPendingMigrations() + { + return $this->countPendingMigrations() > 0; + } + + /** + * @return int + */ + public function countPendingMigrations() + { + return count($this->listPendingMigrations()); + } + + /** + * @return Migration[] + */ + public function getPendingMigrations() + { + $migrations = array(); + foreach ($this->listPendingMigrations() as $version) { + $migrations[] = new Migration( + $version, + $this->loadMigrationFile($version) + ); + } + + return $migrations; + } + + /** + * @return $this + */ + public function applyPendingMigrations() + { + foreach ($this->getPendingMigrations() as $migration) { + $migration->apply($this->db); + } + + return $this; + } + + /** + * @return int[] + */ + public function listPendingMigrations() + { + $lastMigration = $this->getLastMigrationNumber(); + if ($lastMigration === 0) { + return [0]; + } + + return $this->listMigrationsAfter($this->getLastMigrationNumber()); + } + + /** + * @return int[] + */ + public function listAllMigrations() + { + $dir = $this->getMigrationsDirectory(); + $versions = []; + + if (! is_readable($dir)) { + return $versions; + } + + foreach (new DirectoryIterator($dir) as $file) { + if ($file->isDot()) { + continue; + } + + $filename = $file->getFilename(); + if (preg_match('/^upgrade_(\d+)\.sql$/', $filename, $match)) { + $versions[] = (int) $match[1]; + } + } + + sort($versions); + + return $versions; + } + + /** + * @param $version + * @return false|string + */ + public function loadMigrationFile($version) + { + if ($version === 0) { + $filename = $this->getFullSchemaFile(); + } else { + $filename = sprintf( + '%s/upgrade_%d.sql', + $this->getMigrationsDirectory(), + $version + ); + } + + return file_get_contents($filename); + } + + /** + * @param $version + * @return int[] + */ + protected function listMigrationsAfter($version) + { + $filtered = []; + foreach ($this->listAllMigrations() as $available) { + if ($available > $version) { + $filtered[] = $available; + } + } + + return $filtered; + } + + /** + * @param ?string $subDirectory + * @return string + */ + public function getSchemaDirectory($subDirectory = null) + { + if ($subDirectory === null) { + return $this->schemaDirectory; + } else { + return $this->schemaDirectory . '/' . ltrim($subDirectory, '/'); + } + } + + /** + * @return string + */ + public function getMigrationsDirectory() + { + return $this->getSchemaDirectory($this->dbType . '-migrations'); + } + + /** + * @return string + */ + protected function getFullSchemaFile() + { + return $this->getSchemaDirectory( + $this->dbType. '.sql' + ); + } +} diff --git a/vendor/gipfl/diff/LICENSE b/vendor/gipfl/diff/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/diff/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/diff/composer.json b/vendor/gipfl/diff/composer.json new file mode 100644 index 0000000..05c12a7 --- /dev/null +++ b/vendor/gipfl/diff/composer.json @@ -0,0 +1,25 @@ +{ + "name": "gipfl/diff", + "description": "php-diff wrapper supporting ipl/html", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Diff\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "ext-mbstring": "*", + "ipl/html": ">=0.2" + } +} diff --git a/vendor/gipfl/diff/public/css/diff.less b/vendor/gipfl/diff/public/css/diff.less new file mode 100644 index 0000000..1dc2ef6 --- /dev/null +++ b/vendor/gipfl/diff/public/css/diff.less @@ -0,0 +1,133 @@ +@color-diff-ins: @color-ok; +@color-diff-del: @color-critical; +@color-diff-changed-old: fade(@color-critical, 30%); +@color-diff-changed-new: fade(@color-ok, 30%); +@color-diff-green-shade-light: #dfd; +@color-diff-pale-green: #9e9; +@color-diff-text-on-diff: #051030; +@color-change-replace-del: #e99; + +.Differences { + width: 100%; + table-layout: fixed; + empty-cells: show; +} + +.Differences thead { + display: none; +} + +.Differences thead th { + text-align: left; + padding-left: 4 / 14 * 16em; +} + +.Differences tbody th { + text-align: right; + width: 4em; + padding: 1px 2px; + border-right: 1px solid @gray-light; + background: @gray-lightest; + font-weight: normal; + vertical-align: top; +} + +.Differences tbody td { + color: @text-color; + width: 50%; + .preformatted(); + word-break: break-all; +} + +.DifferencesSideBySide { + ins, del { + text-decoration: none; + } + + .ChangeInsert { + td.Left { + background: @gray-lighter; + } + td.Right { + background: @color-diff-changed-new; + color: @color-diff-text-on-diff; + } + } + + .ChangeDelete { + td.Left { + background: @color-diff-changed-old; + color: @color-diff-text-on-diff; + } + td.Right { + background: @gray-lighter; + } + } + + .ChangeReplace { + td.Left { + background: @color-diff-changed-old; + color: @color-diff-text-on-diff; + del { + background: @color-diff-del; + } + } + + td.Right { + background: @color-diff-changed-new; + color: @color-diff-text-on-diff; + ins { + background: @color-diff-ins; + } + } + + } +} + +.Differences .Skipped { + background: @gray-lightest; +} + +.DifferencesInline .ChangeReplace .Left, +.DifferencesInline .ChangeDelete .Left { + background: @color-diff-changed-old; +} + +.DifferencesInline .ChangeReplace .Right, +.DifferencesInline .ChangeInsert .Right { + background: @color-diff-green-shade-light; +} + +.DifferencesInline .ChangeReplace ins { + background: @color-diff-pale-green; +} + +.DifferencesInline .ChangeReplace del { + background: @color-change-replace-del; +} + +.DifferencesInline { + tr td:last-child { + width: 90%; + } + tr th:first-child { + width: 5%; + } + tr th:nth-child(2) { + width: 5%; + } + ins, del { + text-decoration: none; + } +} + +#layout.compact-layout, #layout.default-layout { + &.twocols table.Differences { + th { + font-size: 0.75em; + } + td { + font-size: 0.916em; + } + } +} diff --git a/vendor/gipfl/diff/src/HtmlRenderer/InlineDiff.php b/vendor/gipfl/diff/src/HtmlRenderer/InlineDiff.php new file mode 100644 index 0000000..5df8bc0 --- /dev/null +++ b/vendor/gipfl/diff/src/HtmlRenderer/InlineDiff.php @@ -0,0 +1,10 @@ +<?php + +namespace gipfl\Diff\HtmlRenderer; + +use gipfl\Diff\PhpDiff\Renderer\Html\Inline; +use ipl\Html\ValidHtml; + +class InlineDiff extends Inline implements ValidHtml +{ +} diff --git a/vendor/gipfl/diff/src/HtmlRenderer/SideBySideDiff.php b/vendor/gipfl/diff/src/HtmlRenderer/SideBySideDiff.php new file mode 100644 index 0000000..e2ac5b2 --- /dev/null +++ b/vendor/gipfl/diff/src/HtmlRenderer/SideBySideDiff.php @@ -0,0 +1,10 @@ +<?php + +namespace gipfl\Diff\HtmlRenderer; + +use gipfl\Diff\PhpDiff\Renderer\Html\SideBySide; +use ipl\Html\ValidHtml; + +class SideBySideDiff extends SideBySide implements ValidHtml +{ +} diff --git a/vendor/gipfl/diff/src/PhpDiff.php b/vendor/gipfl/diff/src/PhpDiff.php new file mode 100644 index 0000000..da2cb1f --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff.php @@ -0,0 +1,147 @@ +<?php + +namespace gipfl\Diff; + +use gipfl\Diff\PhpDiff\OpCodeHelper; +use gipfl\Diff\PhpDiff\SequenceMatcher; + +class PhpDiff +{ + /** @var array The "old" sequence to use as the basis for the comparison */ + private $left; + + /** @var array The "new" sequence to generate the changes for */ + private $right; + + /** @var array contains the generated opcodes for the differences between the two items */ + private $groupedCodes; + + /** + * @var array Associative array of the default options available for the diff class and their default value. + */ + private $defaultOptions = [ + 'context' => 3, + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ]; + + /** + * @var array Array of the options that have been applied for generating the diff. + */ + private $options; + + /** + * $left and $right can be strings, arrays of lines, null or any object that + * can be casted to a string + * + * @param mixed $left Left hand (old) side of the comparison + * @param mixed $right Right hand (new) side of the comparison + * @param array $options see $defaultOptions for possible settings + */ + public function __construct($left, $right, array $options = []) + { + $this->setLeftLines($this->wantArray($left)); + $this->setRightLines($this->wantArray($right)); + $this->options = array_merge($this->defaultOptions, $options); + } + + /** + * Get a range of lines from $start to $end from the first comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getLeft($start = 0, $end = null) + { + if ($start === 0 && $end === null) { + return $this->left; + } + + if ($end === null) { + $length = 1; + } else { + $length = $end - $start; + } + + return array_slice($this->left, $start, $length); + } + + /** + * Get a range of lines from $start to $end from the second comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getRight($start = 0, $end = null) + { + if ($start === 0 && $end === null) { + return $this->right; + } + + if ($end === null) { + $length = 1; + } else { + $length = $end - $start; + } + + return array_slice($this->right, $start, $length); + } + + /** + * Generate a list of the compiled and grouped opcodes for the differences between the + * two strings. Generally called by the renderer, this class instantiates the sequence + * matcher and performs the actual diff generation and return an array of the opcodes + * for it. Once generated, the results are cached in the diff class instance. + * + * @return array Array of the grouped opcodes for the generated diff. + */ + public function getGroupedOpcodes() + { + if ($this->groupedCodes === null) { + $this->groupedCodes = $this->fetchGroupedOpCodes(); + } + + return $this->groupedCodes; + } + + protected function fetchGroupedOpCodes() + { + $matcher = new SequenceMatcher($this->left, $this->right, null, $this->options); + return OpCodeHelper::getGroupedOpcodes( + $matcher->getOpcodes(), + $this->options['context'] + ); + } + + protected function wantArray($value) + { + if (empty($value)) { + return []; + } + if (! is_array($value)) { + return explode("\n", (string) $value); + } + + return $value; + } + + protected function setLeftLines(array $lines) + { + $this->left = $lines; + } + + protected function setRightLines(array $lines) + { + $this->right = $lines; + } + +} diff --git a/vendor/gipfl/diff/src/PhpDiff/ArrayHelper.php b/vendor/gipfl/diff/src/PhpDiff/ArrayHelper.php new file mode 100644 index 0000000..2c57c8d --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/ArrayHelper.php @@ -0,0 +1,54 @@ +<?php + +namespace gipfl\Diff\PhpDiff; + +abstract class ArrayHelper +{ + /** + * Helper function that provides the ability to return the value for a key + * in an array of it exists, or if it doesn't then return a default value. + * Essentially cleaner than doing a series of if(isset()) {} else {} calls. + * + * @param array $array The array to search. + * @param string $key The key to check that exists. + * @param mixed $default The value to return as the default value if the key doesn't exist. + * @return mixed The value from the array if the key exists or otherwise the default. + */ + public static function getPropertyOrDefault($array, $key, $default) + { + if (isset($array[$key])) { + return $array[$key]; + } + + return $default; + } + + /** + * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks + * + * @param array $a First array to compare. + * @param array $b Second array to compare. + * @return int -1, 0 or 1, as expected by the usort function. + */ + public static function tupleSort($a, $b) + { + $max = max(count($a), count($b)); + for ($i = 0; $i < $max; ++$i) { + if ($a[$i] < $b[$i]) { + return -1; + } + if ($a[$i] > $b[$i]) { + return 1; + } + } + + if (count($a) === count($b)) { + return 0; + } + if (count($a) < count($b)) { + return -1; + } + + return 1; + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/OpCodeHelper.php b/vendor/gipfl/diff/src/PhpDiff/OpCodeHelper.php new file mode 100644 index 0000000..7b12b1e --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/OpCodeHelper.php @@ -0,0 +1,144 @@ +<?php + +namespace gipfl\Diff\PhpDiff; + +use function count; +use function max; +use function min; + +abstract class OpCodeHelper +{ + /** + * Return a list of all of the opcodes for the differences between the + * two strings. + * + * The nested array returned contains an array describing the opcode + * which includes: + * 0 - The type of tag (as described below) for the opcode. + * 1 - The beginning line in the first sequence. + * 2 - The end line in the first sequence. + * 3 - The beginning line in the second sequence. + * 4 - The end line in the second sequence. + * + * The different types of tags include: + * replace - The string from $i1 to $i2 in $a should be replaced by + * the string in $b from $j1 to $j2. + * delete - The string in $a from $i1 to $j2 should be deleted. + * insert - The string in $b from $j1 to $j2 should be inserted at + * $i1 in $a. + * equal - The two strings with the specified ranges are equal. + * + * @param array $blocks + * @return array Array of the opcodes describing the differences between the strings. + */ + public static function calculateOpCodes(array $blocks) + { + $lastLeftEnd = 0; + $lastRightEnd = 0; + $opCodes = []; + + foreach ($blocks as list($beginLeft, $beginRight, $cntLines)) { + $tag = null; + if ($lastLeftEnd < $beginLeft) { + if ($lastRightEnd < $beginRight) { + $tag = 'replace'; + } else { + $tag = 'delete'; + } + } elseif ($lastRightEnd < $beginRight) { + $tag = 'insert'; + } + + if ($tag) { + $opCodes[] = [$tag, $lastLeftEnd, $beginLeft, $lastRightEnd, $beginRight]; + } + + $lastLeftEnd = $beginLeft + $cntLines; + $lastRightEnd = $beginRight + $cntLines; + + if ($cntLines) { + $opCodes[] = ['equal', $beginLeft, $lastLeftEnd, $beginRight, $lastRightEnd]; + } + } + + return $opCodes; + } + + /** + * Return a series of nested arrays containing different groups of generated + * opcodes for the differences between the strings with up to $context lines + * of surrounding content. + * + * Essentially what happens here is any big equal blocks of strings are stripped + * out, the smaller subsets of changes are then arranged in to their groups. + * This means that the sequence matcher and diffs do not need to include the full + * content of the different files but can still provide context as to where the + * changes are. + * + * @param array $opCodes + * @param int $context The number of lines of context to provide around the groups. + * @return array Nested array of all of the grouped opcodes. + */ + public static function getGroupedOpcodes(array $opCodes, $context = 3) + { + if (empty($opCodes)) { + $opCodes = [ + ['equal', 0, 1, 0, 1] + ]; + } + + if ($opCodes[0][0] === 'equal') { + $opCodes[0] = [ + $opCodes[0][0], + max($opCodes[0][1], $opCodes[0][2] - $context), + $opCodes[0][2], + max($opCodes[0][3], $opCodes[0][4] - $context), + $opCodes[0][4] + ]; + } + + $lastItem = count($opCodes) - 1; + if ($opCodes[$lastItem][0] === 'equal') { + list($tag, $beginLeft, $endLeft, $beginRight, $endRight) = $opCodes[$lastItem]; + $opCodes[$lastItem] = [ + $tag, + $beginLeft, + min($endLeft, $beginLeft + $context), + $beginRight, + min($endRight, $beginRight + $context) + ]; + } + /* + public $type; + public $beginLeft; + public $endLeft; + public $beginRight; + public $endRight; + */ + $maxRange = $context * 2; + $groups = []; + $group = []; + foreach ($opCodes as list($tag, $beginLeft, $endLeft, $beginRight, $endRight)) { + if ($tag === 'equal' && $endLeft - $beginLeft > $maxRange) { + $group[] = [ + $tag, + $beginLeft, + min($endLeft, $beginLeft + $context), + $beginRight, + min($endRight, $beginRight + $context) + ]; + $groups[] = $group; + $group = []; + $beginLeft = max($beginLeft, $endLeft - $context); + $beginRight = max($beginRight, $endRight - $context); + } + $group[] = [$tag, $beginLeft, $endLeft, $beginRight, $endRight]; + } + + if (!empty($group) && !(count($group) === 1 && $group[0][0] === 'equal')) { + $groups[] = $group; + } + + return $groups; + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/Ratio.php b/vendor/gipfl/diff/src/PhpDiff/Ratio.php new file mode 100644 index 0000000..4db1460 --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/Ratio.php @@ -0,0 +1,139 @@ +<?php + +namespace gipfl\Diff\PhpDiff; + +use function count; + +class Ratio +{ + /** + * @var SequenceMatcher + */ + private $matcher; + + /** @var float */ + private $ratio; + + /** @var array */ + private $a; + + /** @var array */ + private $b; + + /** @var array */ + private $fullBCount; + + public function __construct(SequenceMatcher $matcher) + { + $this->matcher = $matcher; + $this->a = $matcher->getLeftSequence(); + $this->b = $matcher->getRightSequence(); + } + + /** + * Return a measure of the similarity between the two sequences. + * This will be a float value between 0 and 1. + * + * Out of all of the ratio calculation functions, this is the most + * expensive to call if getMatchingBlocks or getOpCodes is yet to be + * called. The other calculation methods (quickRatio and realquickRatio) + * can be used to perform quicker calculations but may be less accurate. + * + * The ratio is calculated as (2 * number of matches) / total number of + * elements in both sequences. + * + * @return float The calculated ratio. + */ + public function getRatio() + { + if ($this->ratio === null) { + $matcher = $this->matcher; + $matches = array_reduce($matcher->getMatchingBlocks(), [$this, 'ratioReduce'], 0); + $this->ratio = $this->calculateRatio( + $matches, + count($this->a) + count($this->b) + ); + } + + return $this->ratio; + } + + /** + * Helper function to calculate the number of matches for Ratio(). + * + * @param int $sum The running total for the number of matches. + * @param array $triple Array containing the matching block triple to add to the running total. + * @return int The new running total for the number of matches. + */ + private function ratioReduce($sum, $triple) + { + return $sum + ($triple[count($triple) - 1]); + } + + /** + * Quickly return an upper bound ratio for the similarity of the strings. + * This is quicker to compute than Ratio(). + * + * @return float The calculated ratio. + */ + private function quickRatio() + { + $aLength = count($this->a); + $bLength = count($this->b); + if ($this->fullBCount === null) { + $this->fullBCount = []; + for ($i = 0; $i < $bLength; ++$i) { + $char = $this->b[$i]; + $this->fullBCount[$char] = ArrayHelper::getPropertyOrDefault($this->fullBCount, $char, 0) + 1; + } + } + + $avail = array(); + $matches = 0; + for ($i = 0; $i < $aLength; ++$i) { + $char = $this->a[$i]; + if (isset($avail[$char])) { + $numb = $avail[$char]; + } else { + $numb = ArrayHelper::getPropertyOrDefault($this->fullBCount, $char, 0); + } + $avail[$char] = $numb - 1; + if ($numb > 0) { + ++$matches; + } + } + + $this->calculateRatio($matches, $aLength + $bLength); + } + + /** + * Return an upper bound ratio really quickly for the similarity of the strings. + * This is quicker to compute than Ratio() and quickRatio(). + * + * @return float The calculated ratio. + */ + private function realquickRatio() + { + $aLength = count($this->a); + $bLength = count($this->b); + + return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); + } + + /** + * Helper function for calculating the ratio to measure similarity for the strings. + * The ratio is defined as being 2 * (number of matches / total length) + * + * @param int $matches The number of matches in the two strings. + * @param int $length The length of the two strings. + * @return float The calculated ratio. + */ + private function calculateRatio($matches, $length = 0) + { + if ($length) { + return 2 * ($matches / $length); + } + + return 1; + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/Renderer/AbstractRenderer.php b/vendor/gipfl/diff/src/PhpDiff/Renderer/AbstractRenderer.php new file mode 100644 index 0000000..fc93d8d --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/Renderer/AbstractRenderer.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\Diff\PhpDiff\Renderer; + +use gipfl\Diff\PhpDiff; + +/** + * Abstract class for diff renderers in PHP DiffLib. + */ +abstract class AbstractRenderer +{ + /** @var PhpDiff */ + public $diff; + + /** @var array default options that apply to this renderer */ + protected $defaultOptions = []; + + /** @var array merged (user applied and default) options for the renderer */ + protected $options = []; + + /** + * @param PhpDiff $diff + * @param array $options Optionally, an array of the options for the renderer. + */ + public function __construct(PhpDiff $diff, array $options = []) + { + $this->diff = $diff; + $this->setOptions($options); + } + + /** + * Set the options of the renderer to those supplied in the passed in array. + * Options are merged with the default to ensure that there aren't any missing + * options. + * + * @param array $options + */ + public function setOptions(array $options) + { + $this->options = array_merge($this->defaultOptions, $options); + } + + abstract public function render(); +} diff --git a/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/ArrayRenderer.php b/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/ArrayRenderer.php new file mode 100644 index 0000000..57f6cb4 --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/ArrayRenderer.php @@ -0,0 +1,207 @@ +<?php + +namespace gipfl\Diff\PhpDiff\Renderer\Html; + +use gipfl\Diff\PhpDiff\Renderer\AbstractRenderer; + +/** + * Base renderer for rendering HTML based diffs for PHP DiffLib. + */ +class ArrayRenderer extends AbstractRenderer +{ + /** @var array default options */ + protected $defaultOptions = [ + 'tabSize' => 4 + ]; + + /** + * Render and return an array structure suitable for generating HTML + * based differences. Generally called by subclasses that generate a + * HTML based diff and return an array of the changes to show in the diff. + * + * @return array An array of the generated chances, suitable for presentation in HTML. + */ + public function render() + { + // As we'll be modifying a & b to include our change markers, + // we need to get the contents and store them here. That way + // we're not going to destroy the original data + $a = $this->diff->getLeft(); + $b = $this->diff->getRight(); + + $changes = []; + foreach ($this->diff->getGroupedOpcodes() as $group) { + $changes[] = $this->renderOpcodesGroup($group, $a, $b); + } + return $changes; + } + + protected function insertLineMarkers($line, $start, $end) + { + $last = $end + mb_strlen($line); + + return mb_substr($line, 0, $start) + . "\0" + . mb_substr($line, $start, $last - $start) + . "\1" + . mb_substr($line, $last); + } + + /** + * @param $group + * @param array $a + * @param array $b + * @return array + */ + protected function renderOpcodesGroup($group, array $a, array $b) + { + $blocks = []; + $lastTag = null; + $lastBlock = 0; + foreach ($group as list($tag, $i1, $i2, $j1, $j2)) { + if ($tag === 'replace' && $i2 - $i1 === $j2 - $j1) { + for ($i = 0; $i < ($i2 - $i1); ++$i) { + $fromLine = $a[$i1 + $i]; + $toLine = $b[$j1 + $i]; + + list($start, $end) = $this->getChangeExtent($fromLine, $toLine); + if ($start !== 0 || $end !== 0) { + $a[$i1 + $i] = $this->insertLineMarkers($fromLine, $start, $end); + $b[$j1 + $i] = $this->insertLineMarkers($toLine, $start, $end); + } + } + } + + if ($tag !== $lastTag) { + $blocks[] = [ + 'tag' => $tag, + 'base' => [ + 'offset' => $i1, + 'lines' => [] + ], + 'changed' => [ + 'offset' => $j1, + 'lines' => [] + ] + ]; + $lastBlock = count($blocks) - 1; + } + + $lastTag = $tag; + + if ($tag === 'equal') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines); + $lines = array_slice($b, $j1, ($j2 - $j1)); + $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines); + } else { + if ($tag === 'replace' || $tag === 'delete') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('<del>', '</del>'), $lines); + $blocks[$lastBlock]['base']['lines'] += $lines; + } + + if ($tag === 'replace' || $tag === 'insert') { + $lines = array_slice($b, $j1, ($j2 - $j1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('<ins>', '</ins>'), $lines); + $blocks[$lastBlock]['changed']['lines'] += $lines; + } + } + } + + return $blocks; + } + + /** + * Given two strings, determine where the changes in the two strings + * begin, and where the changes in the two strings end. + * + * @param string $fromLine The first string. + * @param string $toLine The second string. + * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) + */ + private function getChangeExtent($fromLine, $toLine) + { + $start = 0; + $limit = min(strlen($fromLine), strlen($toLine)); + while ($start < $limit && $fromLine[$start] === $toLine[$start]) { + ++$start; + } + $end = -1; + $limit -= $start; + /** @noinspection SubStrUsedAsArrayAccessInspection $end is negative, array index needs PHP >= 7 */ + while (-$end <= $limit && substr($fromLine, $end, 1) === substr($toLine, $end, 1)) { + --$end; + } + return [ + $start, + $end + 1 + ]; + } + + /** + * Format a series of lines suitable for output in a HTML rendered diff. + * This involves replacing tab characters with spaces, making the HTML safe + * for output, ensuring that double spaces are replaced with etc. + * + * @param array $lines lines to format. + * @return array formatted lines. + */ + protected function formatLines($lines) + { + $lines = array_map([$this, 'ExpandTabs'], $lines); + $lines = array_map([$this, 'HtmlSafe'], $lines); + foreach ($lines as &$line) { + $line = preg_replace_callback('# ( +)|^ #', [$this, 'fixSpaces'], $line); + } + return $lines; + } + + /** + * Replace a string containing spaces with a HTML representation using . + * + * @param string[] $matches preg matches. + * @return string HTML representation of the string. + */ + private function fixSpaces(array $matches) + { + $count = 0; + + if (count($matches) > 1) { + $spaces = $matches[1]; + $count = strlen($spaces); + } + + if ($count === 0) { + return ''; + } + + $div = floor($count / 2); + $mod = $count % 2; + return str_repeat(' ', $div).str_repeat(' ', $mod); + } + + /** + * Replace tabs in a single line with a number of spaces as defined by the tabSize option. + * + * @param string $line The containing tabs to convert. + * @return string The line with the tabs converted to spaces. + */ + private function expandTabs($line) + { + return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line); + } + + /** + * Make a string containing HTML safe for output on a page. + * + * @param string $string The string. + * @return string The string with the HTML characters replaced by entities. + */ + private function htmlSafe($string) + { + return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8'); + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/Inline.php b/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/Inline.php new file mode 100644 index 0000000..6587de1 --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/Inline.php @@ -0,0 +1,104 @@ +<?php + +namespace gipfl\Diff\PhpDiff\Renderer\Html; + +/** + * Inline HTML diff generator for PHP DiffLib. + */ +class Inline extends ArrayRenderer +{ + /** + * Render a and return diff with changes between the two sequences + * displayed inline (under each other) + * + * @return string The generated inline diff. + */ + public function render() + { + $changes = parent::render(); + $html = ''; + if (empty($changes)) { + return $html; + } + + $html .= '<table class="Differences DifferencesInline">'; + $html .= '<thead>'; + $html .= '<tr>'; + $html .= '<th>Old</th>'; + $html .= '<th>New</th>'; + $html .= '<th>Differences</th>'; + $html .= '</tr>'; + $html .= '</thead>'; + foreach ($changes as $i => $blocks) { + // If this is a separate block, we're condensing code so output ..., + // indicating a significant portion of the code has been collapsed as + // it is the same + if ($i > 0) { + $html .= '<tbody class="Skipped">'; + $html .= '<th>…</th>'; + $html .= '<th>…</th>'; + $html .= '<td> </td>'; + $html .= '</tbody>'; + } + + foreach ($blocks as $change) { + $html .= '<tbody class="Change'.ucfirst($change['tag']).'">'; + // Equal changes should be shown on both sides of the diff + if ($change['tag'] === 'equal') { + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>' . $fromLine . '</th>'; + $html .= '<th>' . $toLine . '</th>'; + $html .= '<td class="Left">' . $line . '</td>'; + $html .= '</tr>'; + } + } elseif ($change['tag'] === 'insert') { + // Added lines only on the right side + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<th>' . $toLine . '</th>'; + $html .= '<td class="Right"><ins>' . $line . '</ins> </td>'; + $html .= '</tr>'; + } + } elseif ($change['tag'] === 'delete') { + // Show deleted lines only on the left side + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>' . $fromLine . '</th>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"><del>' . $line . '</del> </td>'; + $html .= '</tr>'; + } + } elseif ($change['tag'] === 'replace') { + // Show modified lines on both sides + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>' . $fromLine . '</th>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"><span>' . $line . '</span></td>'; + $html .= '</tr>'; + } + + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<th>' . $toLine . '</th>'; + $html .= '<td class="Right"><span>' . $line . '</span></td>'; + $html .= '</tr>'; + } + } + $html .= '</tbody>'; + } + } + $html .= '</table>'; + + return $html; + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/SideBySide.php b/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/SideBySide.php new file mode 100644 index 0000000..2d16e08 --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/Renderer/Html/SideBySide.php @@ -0,0 +1,121 @@ +<?php + +namespace gipfl\Diff\PhpDiff\Renderer\Html; + +/** + * Side by Side HTML diff generator for PHP DiffLib. + */ +class SideBySide extends ArrayRenderer +{ + /** + * Render a and return diff with changes between the two sequences + * displayed side by side. + * + * @return string The generated side by side diff. + */ + public function render() + { + $changes = parent::render(); + + $html = ''; + if (empty($changes)) { + return $html; + } + + $html .= '<table class="Differences DifferencesSideBySide">'; + $html .= '<thead>'; + $html .= '<tr>'; + $html .= '<th colspan="2">Old Version</th>'; + $html .= '<th colspan="2">New Version</th>'; + $html .= '</tr>'; + $html .= '</thead>'; + foreach ($changes as $i => $blocks) { + if ($i > 0) { + $html .= '<tbody class="Skipped">'; + $html .= '<th>…</th><td> </td>'; + $html .= '<th>…</th><td> </td>'; + $html .= '</tbody>'; + } + + foreach ($blocks as $change) { + $html .= '<tbody class="Change'.ucfirst($change['tag']).'">'; + // Equal changes should be shown on both sides of the diff + if ($change['tag'] === 'equal') { + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>' . $fromLine . '</th>'; + $html .= '<td class="Left"><span>' . $line . '</span> </td>'; + $html .= '<th>' . $toLine . '</th>'; + $html .= '<td class="Right"><span>' . $line . '</span> </td>'; + $html .= '</tr>'; + } + } elseif ($change['tag'] === 'insert') { + // Added lines only on the right side + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"> </td>'; + $html .= '<th>' . $toLine . '</th>'; + $html .= '<td class="Right"><ins>' . $line . '</ins> </td>'; + $html .= '</tr>'; + } + } elseif ($change['tag'] === 'delete') { + // Show deleted lines only on the left side + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>' . $fromLine . '</th>'; + $html .= '<td class="Left"><del>' . $line . '</del> </td>'; + $html .= '<th> </th>'; + $html .= '<td class="Right"> </td>'; + $html .= '</tr>'; + } + } elseif ($change['tag'] === 'replace') { + // Show modified lines on both sides + if (count($change['base']['lines']) >= count($change['changed']['lines'])) { + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><span>' . $line . '</span> </td>'; + if (!isset($change['changed']['lines'][$no])) { + $toLine = ' '; + $changedLine = ' '; + } else { + $toLine = $change['base']['offset'] + $no + 1; + $changedLine = '<span>'.$change['changed']['lines'][$no].'</span>'; + } + $html .= '<th>' . $toLine . '</th>'; + $html .= '<td class="Right">' . $changedLine . '</td>'; + $html .= '</tr>'; + } + } else { + foreach ($change['changed']['lines'] as $no => $changedLine) { + if (!isset($change['base']['lines'][$no])) { + $fromLine = ' '; + $line = ' '; + } else { + $fromLine = $change['base']['offset'] + $no + 1; + $line = '<span>' . $change['base']['lines'][$no] . '</span>'; + } + $html .= '<tr>'; + $html .= '<th>' . $fromLine . '</th>'; + $html .= '<td class="Left"><span>' . $line . '</span> </td>'; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<th>' . $toLine . '</th>'; + $html .= '<td class="Right">' . $changedLine . '</td>'; + $html .= '</tr>'; + } + } + } + $html .= '</tbody>'; + } + } + $html .= '</table>'; + + return $html; + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/Renderer/Text/Context.php b/vendor/gipfl/diff/src/PhpDiff/Renderer/Text/Context.php new file mode 100644 index 0000000..b8d9cad --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/Renderer/Text/Context.php @@ -0,0 +1,98 @@ +<?php + +namespace gipfl\Diff\PhpDiff\Renderer\Text; + +use gipfl\Diff\PhpDiff\Renderer\AbstractRenderer; + +/** + * Context diff generator for PHP DiffLib. + */ +class Context extends AbstractRenderer +{ + /** + * @var array Array of the different opcode tags and how they map to the context diff equivalent. + */ + private $tagMap = [ + 'insert' => '+', + 'delete' => '-', + 'replace' => '!', + 'equal' => ' ' + ]; + + /** + * Render and return a context formatted (old school!) diff file. + * + * @return string The generated context diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach ($opCodes as $group) { + $diff .= "***************\n"; + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if ($i2 - $i1 >= 2) { + $diff .= '*** '.($group[0][1] + 1).','.$i2." ****\n"; + } else { + $diff .= '*** '.$i2." ****\n"; + } + + if ($j2 - $j1 >= 2) { + $separator = '--- '.($j1 + 1).','.$j2." ----\n"; + } else { + $separator = '--- '.$j2." ----\n"; + } + + $hasVisible = false; + foreach ($group as $code) { + if ($code[0] === 'replace' || $code[0] === 'delete') { + $hasVisible = true; + break; + } + } + + if ($hasVisible) { + foreach ($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if ($tag === 'insert') { + continue; + } + $diff .= $this->tagMap[$tag] + . ' ' + . implode("\n" . $this->tagMap[$tag] . ' ', $this->diff->getLeft($i1, $i2)) + . "\n"; + } + } + + $hasVisible = false; + foreach ($group as $code) { + if ($code[0] === 'replace' || $code[0] === 'insert') { + $hasVisible = true; + break; + } + } + + $diff .= $separator; + + if ($hasVisible) { + foreach ($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if ($tag === 'delete') { + continue; + } + $diff .= $this->tagMap[$tag] + . ' ' + . implode("\n" . $this->tagMap[$tag] . ' ', $this->diff->getRight($j1, $j2)) + . "\n"; + } + } + } + + return $diff; + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/Renderer/Text/Unified.php b/vendor/gipfl/diff/src/PhpDiff/Renderer/Text/Unified.php new file mode 100644 index 0000000..afeb96d --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/Renderer/Text/Unified.php @@ -0,0 +1,52 @@ +<?php + +namespace gipfl\Diff\PhpDiff\Renderer\Text; + +use gipfl\Diff\PhpDiff\Renderer\AbstractRenderer; + +/** + * Unified diff generator for PHP DiffLib. + */ +class Unified extends AbstractRenderer +{ + /** + * Render and return a unified diff. + * + * @return string The unified diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach ($opCodes as $group) { + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if ($i1 === 0 && $i2 === 0) { + $i1 = -1; + $i2 = -1; + } + + $diff .= '@@ -' . ($i1 + 1) . ',' . ($i2 - $i1) . ' +' . ($j1 + 1) . ',' . ($j2 - $j1) . " @@\n"; + foreach ($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if ($tag === 'equal') { + $diff .= ' ' . implode("\n ", $this->diff->getLeft($i1, $i2))."\n"; + } else { + if ($tag === 'replace' || $tag === 'delete') { + $diff .= '-' . implode("\n-", $this->diff->getLeft($i1, $i2))."\n"; + } + + if ($tag === 'replace' || $tag === 'insert') { + $diff .= '+' . implode("\n+", $this->diff->getRight($j1, $j2))."\n"; + } + } + } + } + + return $diff; + } +} diff --git a/vendor/gipfl/diff/src/PhpDiff/SequenceMatcher.php b/vendor/gipfl/diff/src/PhpDiff/SequenceMatcher.php new file mode 100644 index 0000000..a185762 --- /dev/null +++ b/vendor/gipfl/diff/src/PhpDiff/SequenceMatcher.php @@ -0,0 +1,438 @@ +<?php + +namespace gipfl\Diff\PhpDiff; + +use function array_merge; +use function count; +use function is_array; +use function str_replace; +use function str_split; +use function strtolower; + +/** + * Sequence matcher for Diff + */ +class SequenceMatcher +{ + /** + * Either a string or an array containing a callback function to determine + * if a line is "junk" or not + * + * @var string|array + */ + private $junkCallback; + + /** + * @var array The first sequence to compare against. + */ + private $left = []; + + /** + * @var array The second sequence. + */ + private $right = []; + + /** + * @var array Characters that are considered junk from the second sequence. Characters are the array key. + */ + private $junkCharacters = []; + + /** + * @var array Array of indices that do not contain junk elements. + */ + private $b2j = []; + + private $options = []; + + private $defaultOptions = [ + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ]; + + /** @var array|null */ + private $matchingBlocks; + + /** @var array|null */ + private $opCodes; + + /** + * The constructor. With the sequences being passed, they'll be set for the + * sequence matcher and it will perform a basic cleanup & calculate junk + * elements. + * + * @param string|array $left A string or array containing the lines to compare against. + * @param string|array $right A string or array containing the lines to compare. + * @param string|array $junkCallback Either an array or string that references a callback + * function (if there is one) to determine 'junk' characters. + * @param array $options + */ + public function __construct($left, $right, $junkCallback = null, $options = []) + { + $this->junkCallback = $junkCallback; + $this->setOptions($options); + $this->setSequences($left, $right); + } + + public function setOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + } + + /** + * Set the first and second sequences to use with the sequence matcher. + * + * @param string|array $left A string or array containing the lines to compare against. + * @param string|array $right A string or array containing the lines to compare. + */ + public function setSequences($left, $right) + { + $this->setLeftSequence($left); + $this->setRightSequence($right); + } + + /** + * Set the first sequence and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $sequence The sequence to set as the first sequence. + */ + protected function setLeftSequence($sequence) + { + if (!is_array($sequence)) { + $sequence = str_split($sequence); + } + if ($sequence === $this->left) { + return; + } + + $this->resetCalculation(); + $this->left = $sequence; + } + + /** + * Set the second sequence ($b) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $sequence The sequence to set as the second sequence. + */ + protected function setRightSequence($sequence) + { + if (!is_array($sequence)) { + $sequence = str_split($sequence); + } + if ($sequence === $this->right) { + return; + } + + $this->resetCalculation(); + $this->right = $sequence; + $this->generateRightChain(); + } + + protected function resetCalculation() + { + $this->matchingBlocks = null; + $this->opCodes = null; + } + + /** + * @return array + */ + public function getLeftSequence() + { + return $this->left; + } + + /** + * @return array + */ + public function getRightSequence() + { + return $this->right; + } + + /** + * Generate the internal arrays containing the list of junk and non-junk + * characters for the second ($b) sequence. + */ + private function generateRightChain() + { + $length = count($this->right); + $this->b2j = []; + $popularDict = []; + + foreach ($this->right as $i => $char) { + if (isset($this->b2j[$char])) { + if ($length >= 200 && count($this->b2j[$char]) * 100 > $length) { + $popularDict[$char] = 1; + unset($this->b2j[$char]); + } else { + $this->b2j[$char][] = $i; + } + } else { + $this->b2j[$char] = [$i]; + } + } + + // Remove leftovers + foreach (array_keys($popularDict) as $char) { + unset($this->b2j[$char]); + } + + $this->junkCharacters = []; + if (is_callable($this->junkCallback)) { + foreach (array_keys($popularDict) as $char) { + if (call_user_func($this->junkCallback, $char)) { + $this->junkCharacters[$char] = 1; + unset($popularDict[$char]); + } + } + + foreach (array_keys($this->b2j) as $char) { + if (call_user_func($this->junkCallback, $char)) { + $this->junkCharacters[$char] = 1; + unset($this->b2j[$char]); + } + } + } + } + + /** + * Checks if a particular character is in the junk dictionary + * for the list of junk characters. + * + * @param $b + * @return boolean whether the character is considered junk + */ + private function isBJunk($b) + { + if (isset($this->junkCharacters[$b])) { + return true; + } + + return false; + } + + /** + * Find the longest matching block in the two sequences, as defined by the + * lower and upper constraints for each sequence. (for the first sequence, + * $alo - $ahi and for the second sequence, $blo - $bhi) + * + * Essentially, of all of the maximal matching blocks, return the one that + * starts earliest in $a, and all of those maximal matching blocks that + * start earliest in $a, return the one that starts earliest in $b. + * + * If the junk callback is defined, do the above but with the restriction + * that the junk element appears in the block. Extend it as far as possible + * by matching only junk elements in both $a and $b. + * + * @param int $beginLeft The lower constraint for the first sequence. + * @param int $endLeft The upper constraint for the first sequence. + * @param int $beginRight The lower constraint for the second sequence. + * @param int $endRight The upper constraint for the second sequence. + * @return array Array containing the longest match that includes the starting + * position in $a, start in $b and the length/size. + */ + public function findLongestMatch($beginLeft, $endLeft, $beginRight, $endRight) + { + $left = $this->left; + $right = $this->right; + + $bestBeginLeft = $beginLeft; + $bestBeginRight = $beginRight; + $bestSize = 0; + + $j2Len = []; + $nothing = []; + + for ($currentLeft = $beginLeft; $currentLeft < $endLeft; ++$currentLeft) { + $newJ2Len = []; + $junkList = ArrayHelper::getPropertyOrDefault($this->b2j, $left[$currentLeft], $nothing); + foreach ($junkList as $junk) { + if ($junk < $beginRight) { + continue; + } + if ($junk >= $endRight) { + break; + } + + $k = ArrayHelper::getPropertyOrDefault($j2Len, $junk -1, 0) + 1; + $newJ2Len[$junk] = $k; + if ($k > $bestSize) { + $bestBeginLeft = $currentLeft - $k + 1; + $bestBeginRight = $junk - $k + 1; + $bestSize = $k; + } + } + + $j2Len = $newJ2Len; + } + + while ($bestBeginLeft > $beginLeft + && $bestBeginRight > $beginRight + && !$this->isBJunk($right[$bestBeginRight - 1]) + && !$this->linesAreDifferent($bestBeginLeft - 1, $bestBeginRight - 1) + ) { + --$bestBeginLeft; + --$bestBeginRight; + ++$bestSize; + } + + while ($bestBeginLeft + $bestSize < $endLeft && ($bestBeginRight + $bestSize) < $endRight + && !$this->isBJunk($right[$bestBeginRight + $bestSize]) + && !$this->linesAreDifferent($bestBeginLeft + $bestSize, $bestBeginRight + $bestSize) + ) { + ++$bestSize; + } + + while ($bestBeginLeft > $beginLeft + && $bestBeginRight > $beginRight + && $this->isBJunk($right[$bestBeginRight - 1]) + && !$this->linesAreDifferent($bestBeginLeft - 1, $bestBeginRight - 1) + ) { + --$bestBeginLeft; + --$bestBeginRight; + ++$bestSize; + } + + while ($bestBeginLeft + $bestSize < $endLeft + && $bestBeginRight + $bestSize < $endRight + && $this->isBJunk($right[$bestBeginRight + $bestSize]) + && !$this->linesAreDifferent($bestBeginLeft + $bestSize, $bestBeginRight + $bestSize) + ) { + ++$bestSize; + } + + return [$bestBeginLeft, $bestBeginRight, $bestSize]; + } + + /** + * Check if the two lines at the given indexes are different or not. + * + * @param int $leftIndex Line number to check against in a. + * @param int $rightIndex Line number to check against in b. + * @return boolean True if the lines are different and false if not. + */ + public function linesAreDifferent($leftIndex, $rightIndex) + { + $leftLine = $this->left[$leftIndex]; + $rightLine = $this->right[$rightIndex]; + + if ($this->options['ignoreWhitespace']) { + $replace = ["\t", ' ']; + $leftLine = str_replace($replace, '', $leftLine); + $rightLine = str_replace($replace, '', $rightLine); + } + + if ($this->options['ignoreCase']) { + $leftLine = strtolower($leftLine); + $rightLine = strtolower($rightLine); + } + + return $leftLine !== $rightLine; + } + + /** + * Return a nested set of arrays for all of the matching sub-sequences + * in the strings $a and $b. + * + * Each block contains the lower constraint of the block in $a, the lower + * constraint of the block in $b and finally the number of lines that the + * block continues for. + * + * @return array Nested array of the matching blocks, as described by the function. + */ + public function getMatchingBlocks() + { + if ($this->matchingBlocks === null) { + $this->matchingBlocks = $this->calculateMatchingBlocks(); + } + + return $this->matchingBlocks; + } + + public function calculateMatchingBlocks() + { + $leftLength = count($this->left); + $rightLength = count($this->right); + + $queue = [ + [0, $leftLength, 0, $rightLength] + ]; + + $matchingBlocks = []; + while (!empty($queue)) { + list($leftBegin, $leftEnd, $rightBegin, $rightEnd) = array_pop($queue); + $block = $this->findLongestMatch($leftBegin, $leftEnd, $rightBegin, $rightEnd); + list($bestBeginLeft, $bestBeginRight, $bestSize) = $block; + if ($bestSize) { + $matchingBlocks[] = $block; + if ($leftBegin < $bestBeginLeft && $rightBegin < $bestBeginRight) { + $queue[] = [ + $leftBegin, + $bestBeginLeft, + $rightBegin, + $bestBeginRight + ]; + } + + if ($bestBeginLeft + $bestSize < $leftEnd && $bestBeginRight + $bestSize < $rightEnd) { + $queue[] = [ + $bestBeginLeft + $bestSize, + $leftEnd, + $bestBeginRight + $bestSize, + $rightEnd + ]; + } + } + } + + usort($matchingBlocks, [ArrayHelper::class, 'tupleSort']); + + return static::getNonAdjacentBlocks($matchingBlocks, $leftLength, $rightLength); + } + + public function getOpcodes() + { + if ($this->opCodes === null) { + $this->opCodes = OpCodeHelper::calculateOpCodes($this->getMatchingBlocks()); + } + + return $this->opCodes; + } + + /** + * @param array $matchingBlocks + * @param $leftLength + * @param $rightLength + * @return array + */ + protected static function getNonAdjacentBlocks(array $matchingBlocks, $leftLength, $rightLength) + { + $newLeft = 0; + $newRight = 0; + $newCnt = 0; + $nonAdjacent = []; + foreach ($matchingBlocks as list($beginLeft, $beginRight, $cntLines)) { + if ($newLeft + $newCnt === $beginLeft && $newRight + $newCnt === $beginRight) { + $newCnt += $cntLines; + } else { + if ($newCnt) { + $nonAdjacent[] = [$newLeft, $newRight, $newCnt]; + } + + $newLeft = $beginLeft; + $newRight = $beginRight; + $newCnt = $cntLines; + } + } + + if ($newCnt) { + $nonAdjacent[] = [$newLeft, $newRight, $newCnt]; + } + + $nonAdjacent[] = [$leftLength, $rightLength, 0]; + return $nonAdjacent; + } +} diff --git a/vendor/gipfl/format/composer.json b/vendor/gipfl/format/composer.json new file mode 100644 index 0000000..10c3b90 --- /dev/null +++ b/vendor/gipfl/format/composer.json @@ -0,0 +1,28 @@ +{ + "name": "gipfl/format", + "description": "Arbitrary collection of Format helpers", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Format\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "ext-intl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^7.5 || ^6.5 || ^5.7", + "squizlabs/php_codesniffer": "^3.6" + } +} diff --git a/vendor/gipfl/format/src/LocalDateFormat.php b/vendor/gipfl/format/src/LocalDateFormat.php new file mode 100644 index 0000000..60a29e4 --- /dev/null +++ b/vendor/gipfl/format/src/LocalDateFormat.php @@ -0,0 +1,41 @@ +<?php + +namespace gipfl\Format; + +use IntlDateFormatter; + +class LocalDateFormat +{ + use LocaleAwareness; + + /** @var IntlDateFormatter */ + protected $formatter; + + /** + * @param $time + * @return string + */ + public function getFullDay($time) + { + return $this->formatter()->format($time); + } + + protected function formatter() + { + if ($this->formatter === null) { + $this->formatter = new IntlDateFormatter( + $this->getLocale(), + IntlDateFormatter::FULL, + IntlDateFormatter::NONE + ); + $this->formatter->setTimeZone($this->getTimezone()); + } + + return $this->formatter; + } + + protected function reset() + { + $this->formatter = null; + } +} diff --git a/vendor/gipfl/format/src/LocalTimeFormat.php b/vendor/gipfl/format/src/LocalTimeFormat.php new file mode 100644 index 0000000..1ea3ce7 --- /dev/null +++ b/vendor/gipfl/format/src/LocalTimeFormat.php @@ -0,0 +1,184 @@ +<?php + +namespace gipfl\Format; + +use IntlDateFormatter; +use RuntimeException; + +class LocalTimeFormat +{ + use LocaleAwareness; + + /** @var IntlDateFormatter */ + protected $formatter; + + /** + * For available symbols please see: + * https://unicode-org.github.io/icu/userguide/format_parse/datetime/#date-field-symbol-table + * + * @param int|float $time Hint: also supports DateTime, DateTimeInterface since 7.1.5 + * @return string + */ + public function format($time, $pattern) + { + $result = $this->formatter($pattern)->format($time); + if ($result === false) { + throw new RuntimeException(sprintf( + 'Failed to format %s as "%s": %s (%d)', + $time, + $pattern, + $this->formatter->getErrorMessage(), + $this->formatter->getErrorCode() + )); + } + + return $result; + } + + /** + * @param $time + * @return string + */ + public function getWeekOfYear($time) + { + return $this->format($time, 'ww'); + } + + /** + * @param $time + * @return int + */ + public function getNumericWeekOfYear($time) + { + return (int) $this->format($time, 'w'); + } + + /** + * @param $time + * @return string + */ + public function getDayInMonth($time) + { + return $this->format($time, 'dd'); + } + + /** + * @param $time + * @return int + */ + public function getNumericDayInMonth($time) + { + return (int) $this->format($time, 'd'); + } + + /** + * @param $time + * @return string + */ + public function getWeekdayName($time) + { + return $this->format($time, 'cccc'); + } + + /** + * @param $time + * @return string + */ + public function getShortWeekdayName($time) + { + return $this->format($time, 'ccc'); + } + + /** + * e.g. September + * + * @param $time + * @return string + */ + public function getMonthName($time) + { + return $this->format($time, 'LLLL'); + } + + /** + * e.g. Sep + * + * @param $time + * @return string + */ + public function getShortMonthName($time) + { + return $this->format($time, 'LLL'); + } + + /** + * e.g. 2021 + * @param $time + * @return string + */ + public function getYear($time) + { + return $this->format($time, 'y'); + } + + /** + * e.g. 21 + * + * @param $time + * @return string + */ + public function getShortYear($time) + { + return $this->format($time, 'yy'); + } + + /** + * e.g. 21:50:12 + * + * @param $time + * @return string + */ + public function getTime($time) + { + if ($this->wantsAmPm()) { + return $this->format($time, 'h:mm:ss a'); + } + + return $this->format($time, 'H:mm:ss'); + } + + /** + * e.g. 21:50 + * + * @param $time + * @return string + */ + public function getShortTime($time) + { + if ($this->wantsAmPm()) { + return $this->format($time, 'K:mm a'); + } + + return $this->format($time, 'H:mm'); + } + + protected function formatter($pattern) + { + if ($this->formatter === null) { + $this->formatter = new IntlDateFormatter( + $this->getLocale(), + IntlDateFormatter::GREGORIAN, + IntlDateFormatter::GREGORIAN + ); + $this->formatter->setTimeZone($this->getTimezone()); + } + $this->formatter->setPattern($pattern); + + return $this->formatter; + } + + protected function reset() + { + $this->formatter = null; + } +} diff --git a/vendor/gipfl/format/src/LocaleAwareness.php b/vendor/gipfl/format/src/LocaleAwareness.php new file mode 100644 index 0000000..cb1a417 --- /dev/null +++ b/vendor/gipfl/format/src/LocaleAwareness.php @@ -0,0 +1,94 @@ +<?php + +namespace gipfl\Format; + +use DateTimeZone; +use IntlTimeZone; +use RuntimeException; + +trait LocaleAwareness +{ + /** @var string */ + protected $locale; + + /** @var DateTimeZone|IntlTimeZone */ + protected $timezone; + + /** + * @param string $locale + * @return void + */ + public function setLocale($locale) + { + if ($this->locale !== $locale) { + $this->locale = (string) $locale; + $this->reset(); + } + } + + /** + * @param DateTimeZone|IntlTimeZone $timezone + * @return void + */ + public function setTimezone($timezone) + { + // Hint: type checking is delegated to timeZonesAreEqual + if (self::timeZonesAreEqual($this->timezone, $timezone)) { + return; + } + + $this->timezone = $timezone; + $this->reset(); + } + + protected function wantsAmPm() + { + // TODO: complete this list + return in_array($this->getLocale(), ['en_US', 'en_US.UTF-8']); + } + + protected function isUsEnglish() + { + return in_array($this->getLocale(), ['en_US', 'en_US.UTF-8']); + } + + protected function getLocale() + { + if ($this->locale === null) { + $this->locale = setlocale(LC_TIME, 0) ?: 'C'; + } + + return $this->locale; + } + + protected function getTimezone() + { + if ($this->timezone === null) { + $this->timezone = new DateTimeZone(date_default_timezone_get()); + } + + return $this->timezone; + } + + protected static function timeZonesAreEqual($left, $right) + { + if ($left === null && $right !== null) { + return false; + } + if ($left !== null && $right === null) { + return false; + } + if ($left instanceof DateTimeZone) { + return $right instanceof DateTimeZone && $left->getName() === $right->getName(); + } + if ($left instanceof IntlTimeZone) { + return $right instanceof IntlTimeZone && $left->getID() === $right->getID(); + } + + throw new RuntimeException(sprintf( + 'Valid timezone expected, got left=%s, right=%s', + is_object($left) ? get_class($left) : gettype($left), + is_object($right) ? get_class($right) : gettype($right) + )); + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/composer.json b/vendor/gipfl/icinga-cli-daemon/composer.json new file mode 100644 index 0000000..ec38ba2 --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/composer.json @@ -0,0 +1,24 @@ +{ + "name": "gipfl/icinga-cli-daemon", + "type": "library", + "description": "Helpers for Icinga CLI Daemons", + "homepage": "https://github.com/gipfl/icinga-cli-daemon", + "config": { + "sort-packages": true, + "platform": { + "php": "5.6.3" + } + }, + "autoload": { + "psr-4": { + "gipfl\\IcingaCliDaemon\\": "src/" + } + }, + "require": { + "php": ">=5.6.3", + "ext-posix": "*", + "gipfl/cli": ">=0.5.0", + "react/event-loop": ">=1.1", + "react/promise": ">=2.7" + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/src/DbResourceConfigWatch.php b/vendor/gipfl/icinga-cli-daemon/src/DbResourceConfigWatch.php new file mode 100644 index 0000000..08b188f --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/src/DbResourceConfigWatch.php @@ -0,0 +1,163 @@ +<?php + +namespace gipfl\IcingaCliDaemon; + +use Icinga\Application\Config; +use InvalidArgumentException; +use React\EventLoop\LoopInterface; + +/** + * DbResourceConfigWatch + * + * Checks every $interval = 3 seconds for changed DB resource configuration. + * Notifies registered callbacksin case this happens. + */ +class DbResourceConfigWatch +{ + /** @var string */ + protected $configFile; + + /** @var string */ + protected $resourceConfigFile; + + /** @var string|null */ + protected $dbResourceName; + + /** @var array|null|false It's false on initialization to trigger */ + protected $resourceConfig = false; + + /** @var int|float */ + protected $interval = 3; + + /** @var callable[] */ + protected $callbacks = []; + + /** + * @param string $dbResourceName + * @return DbResourceConfigWatch + */ + public static function name($dbResourceName) + { + $self = new static(); + $self->dbResourceName = $dbResourceName; + + return $self; + } + + /** + * @param string $moduleName + * @return DbResourceConfigWatch + */ + public static function module($moduleName) + { + $self = new static(); + $self->configFile = Config::module($moduleName)->getConfigFile(); + $self->resourceConfigFile = Config::app('resources')->getConfigFile(); + + return $self; + } + + /** + * @param int|float $interval + * @return $this + */ + public function setInterval($interval) + { + if (! \is_int($interval) && ! \is_float($interval)) { + throw new InvalidArgumentException( + '$interval needs to be either int or float' + ); + } + $this->interval = $interval; + + return $this; + } + + /** + * @param callable $callable + * @return $this + */ + public function notify($callable) + { + if (! \is_callable($callable)) { + throw new InvalidArgumentException('$callable needs to be callable'); + } + $this->callbacks[] = $callable; + + return $this; + } + + /** + * @param LoopInterface $loop + */ + public function run(LoopInterface $loop) + { + $check = function () { + $this->checkForFreshConfig(); + }; + $loop->addPeriodicTimer($this->interval, $check); + $loop->futureTick($check); + } + + protected function checkForFreshConfig() + { + if ($this->configHasBeenChanged()) { + $this->emitNewConfig($this->resourceConfig); + } + } + + protected function emitNewConfig($config) + { + foreach ($this->callbacks as $callback) { + $callback($config); + } + } + + protected function getResourceName() + { + if ($this->dbResourceName) { + return $this->dbResourceName; + } else { + return $this->loadDbResourceName(); + } + } + + protected function loadDbResourceName() + { + $parsed = @\parse_ini_file($this->configFile, true); + if (isset($parsed['db']['resource'])) { + return $parsed['db']['resource']; + } else { + return null; + } + } + + protected function loadDbConfigFromDisk($name) + { + if ($name === null) { + return null; + } + + $parsed = @\parse_ini_file($this->resourceConfigFile, true); + if (isset($parsed[$name])) { + $section = $parsed[$name]; + \ksort($section); + + return $section; + } else { + return null; + } + } + + protected function configHasBeenChanged() + { + $resource = $this->loadDbConfigFromDisk($this->loadDbResourceName()); + if ($resource !== $this->resourceConfig) { + $this->resourceConfig = $resource; + + return true; + } else { + return false; + } + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/src/FinishedProcessState.php b/vendor/gipfl/icinga-cli-daemon/src/FinishedProcessState.php new file mode 100644 index 0000000..9ca5e5f --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/src/FinishedProcessState.php @@ -0,0 +1,66 @@ +<?php + +namespace gipfl\IcingaCliDaemon; + +class FinishedProcessState +{ + /** @var int|null */ + protected $exitCode; + + /** @var int|null */ + protected $termSignal; + + public function __construct($exitCode, $termSignal) + { + $this->exitCode = $exitCode; + $this->termSignal = $termSignal; + } + + public function succeeded() + { + return $this->exitCode === 0; + } + + /** + * @return int|null + */ + public function getExitCode() + { + return $this->exitCode; + } + + public function getTermSignal() + { + return $this->termSignal; + } + + public function getCombinedExitCode() + { + if ($this->exitCode === null) { + if ($this->termSignal === null) { + return 255; + } else { + return 255 + $this->termSignal; + } + } else { + return $this->exitCode; + } + } + + public function getReason() + { + if ($this->exitCode === null) { + if ($this->termSignal === null) { + return 'Process died'; + } else { + return 'Process got terminated with SIGNAL ' . $this->termSignal; + } + } else { + if ($this->exitCode === 0) { + return 'Process finished successfully'; + } else { + return 'Process exited with exit code ' . $this->exitCode; + } + } + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/src/IcingaCli.php b/vendor/gipfl/icinga-cli-daemon/src/IcingaCli.php new file mode 100644 index 0000000..8aad4c6 --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/src/IcingaCli.php @@ -0,0 +1,144 @@ +<?php + +namespace gipfl\IcingaCliDaemon; + +use Exception; +use Evenement\EventEmitterTrait; +use gipfl\Protocol\JsonRpc\Connection; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise\Stream; + +class IcingaCli +{ + use EventEmitterTrait; + + /** @var IcingaCliRunner */ + protected $runner; + + /** @var Connection|null */ + protected $rpc; + + protected $arguments = []; + + /** @var \React\Stream\WritableStreamInterface|null */ + protected $stdin; + + /** @var Deferred|null */ + protected $deferredStdin; + + /** @var \React\Stream\ReadableStreamInterface|null */ + protected $stdout; + + /** @var Deferred|null */ + protected $deferredStdout; + + /** @var \React\Stream\ReadableStreamInterface|null */ + protected $stderr; + + /** @var Deferred|null */ + protected $deferredStderr; + + public function __construct(IcingaCliRunner $runner = null) + { + if ($runner === null) { + $runner = new IcingaCliRunner(); + } + $this->runner = $runner; + $this->init(); + } + + protected function init() + { + // Override this if you want. + } + + public function setArguments($arguments) + { + $this->arguments = $arguments; + + return $this; + } + + public function getArguments() + { + return $this->arguments; + } + + public function run(LoopInterface $loop) + { + $process = $this->runner->command($this->getArguments()); + $canceller = function () use ($process) { + // TODO: first soft, then hard + $process->terminate(); + }; + $deferred = new Deferred($canceller); + $process->on('exit', function ($exitCode, $termSignal) use ($deferred) { + $state = new FinishedProcessState($exitCode, $termSignal); + if ($state->succeeded()) { + $deferred->resolve(); + } else { + $deferred->reject(new Exception($state->getReason())); + } + }); + + $process->start($loop); + if ($this->deferredStdin instanceof Deferred) { + $this->deferredStdin->resolve($process->stdin); + } else { + $this->stdin = $process->stdin; + } + if ($this->deferredStdout instanceof Deferred) { + $this->deferredStdout->resolve($process->stdout); + } else { + $this->stdout = $process->stdout; + } + if ($this->deferredStderr instanceof Deferred) { + $this->deferredStderr->resolve($process->stderr); + } else { + $this->stderr = $process->stderr; + } + $this->emit('start', [$process]); + + return $deferred->promise(); + } + + /** + * @return \React\Stream\WritableStreamInterface + */ + public function stdin() + { + if ($this->stdin === null) { + $this->deferredStdin = new Deferred(); + $this->stdin = Stream\unwrapWritable($this->deferredStdin->promise()); + } + + return $this->stdin; + } + + /** + * @return \React\Stream\ReadableStreamInterface + */ + public function stdout() + { + if ($this->stdout === null) { + $this->deferredStdout = new Deferred(); + $this->stdout = Stream\unwrapReadable($this->deferredStdout->promise()); + } + + return $this->stdout; + } + + /** + * @return \React\Stream\ReadableStreamInterface + */ + public function stderr() + { + if ($this->stderr === null) { + $this->deferredStderr = new Deferred(); + $this->stderr = Stream\unwrapReadable($this->deferredStderr->promise()); + } + + return $this->stderr; + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRpc.php b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRpc.php new file mode 100644 index 0000000..473d4ed --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRpc.php @@ -0,0 +1,45 @@ +<?php + +namespace gipfl\IcingaCliDaemon; + +use Exception; +use gipfl\Protocol\JsonRpc\Connection; +use gipfl\Protocol\NetString\StreamWrapper; +use React\ChildProcess\Process; + +class IcingaCliRpc extends IcingaCli +{ + /** @var IcingaCliRunner */ + protected $runner; + + /** @var Connection|null */ + protected $rpc; + + protected $arguments = []; + + protected function init() + { + $this->on('start', function (Process $process) { + $netString = new StreamWrapper( + $process->stdout, + $process->stdin + ); + $netString->on('error', function (Exception $e) { + $this->emit('error', [$e]); + }); + $this->rpc()->handle($netString); + }); + } + + /** + * @return Connection + */ + public function rpc() + { + if ($this->rpc === null) { + $this->rpc = new Connection(); + } + + return $this->rpc; + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRunner.php b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRunner.php new file mode 100644 index 0000000..60a25cc --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRunner.php @@ -0,0 +1,88 @@ +<?php + +namespace gipfl\IcingaCliDaemon; + +use React\ChildProcess\Process; +use gipfl\Cli\Process as CliProcess; + +class IcingaCliRunner +{ + /** @var string */ + protected $binary; + + /** @var string|null */ + protected $cwd; + + /** @var array|null */ + protected $env; + + public function __construct($binary = null) + { + if ($binary === null) { + $this->binary = CliProcess::getBinaryPath(); + $this->cwd = CliProcess::getInitialCwd(); + } else { + $this->binary = $binary; + } + } + + /** + * @param mixed array|...$arguments + * @return Process + */ + public function command($arguments = null) + { + if (! \is_array($arguments)) { + $arguments = \func_get_args(); + } + + return new Process( + $this->escapedCommand($arguments), + $this->cwd, + $this->env + ); + } + + /** + * @param string|null $cwd + */ + public function setCwd($cwd) + { + if ($cwd === null) { + $this->cwd = $cwd; + } else { + $this->cwd = (string) $cwd; + } + } + + /** + * @param array|null $env + */ + public function setEnv($env) + { + if ($env === null) { + $this->env = $env; + } else { + $this->env = (array) $env; + } + } + + /** + * @param $arguments + * @return string + */ + protected function escapedCommand($arguments) + { + $command = ['exec', \escapeshellcmd($this->binary)]; + + foreach ($arguments as $argument) { + if (\ctype_alnum(preg_replace('/^-{1,2}/', '', $argument))) { + $command[] = $argument; + } else { + $command[] = \escapeshellarg($argument); + } + } + + return \implode(' ', $command); + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/src/RetryUnless.php b/vendor/gipfl/icinga-cli-daemon/src/RetryUnless.php new file mode 100644 index 0000000..0303b2c --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/src/RetryUnless.php @@ -0,0 +1,244 @@ +<?php + +namespace gipfl\IcingaCliDaemon; + +use Exception; +use Icinga\Application\Logger; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; +use RuntimeException; + +class RetryUnless +{ + /** @var LoopInterface */ + protected $loop; + + /** @var Deferred */ + protected $deferred; + + /** @var TimerInterface */ + protected $timer; + + /** @var callable */ + protected $callback; + + /** @var bool */ + protected $expectsSuccess; + + /** @var int Regular interval */ + protected $interval = 1; + + /** @var int|null Optional, interval will be changed after $burst attempts */ + protected $burst = null; + + /** @var int|null Interval after $burst attempts */ + protected $finalInterval = null; + + /** @var int Current attempt count */ + protected $attempts = 0; + + /** @var bool No attempts will be made while paused */ + protected $paused = false; + + protected $lastError; + + protected function __construct($callback, $expectsSuccess = true) + { + $this->callback = $callback; + $this->expectsSuccess = $expectsSuccess; + } + + public static function succeeding($callback) + { + return new static($callback); + } + + public static function failing($callback) + { + return new static($callback, false); + } + + public function run(LoopInterface $loop) + { + $this->assertNotRunning(); + $this->deferred = $deferred = new Deferred(); + $this->loop = $loop; + $loop->futureTick(function () { + $this->nextAttempt(); + }); + + return $deferred->promise(); + } + + public function getLastError() + { + return $this->lastError; + } + + public function setInterval($interval) + { + $this->interval = $interval; + + return $this; + } + + public function slowDownAfter($burst, $interval) + { + $this->burst = $burst; + $this->finalInterval = $interval; + + return $this; + } + + public function pause() + { + $this->removeEventualTimer(); + $this->paused = true; + + return $this; + } + + public function resume() + { + if ($this->paused) { + $this->paused = false; + if ($this->timer === null) { + $this->nextAttempt(); + } + } + } + + public function reset() + { + $this->attempts = 0; + $this->paused = false; + $this->removeEventualTimer(); + $this->rejectEventualDeferred('RetryUnless has been reset'); + + return $this; + } + + public function getAttempts() + { + return $this->attempts; + } + + protected function nextAttempt() + { + if ($this->paused) { + return; + } + + $this->removeEventualTimer(); + $this->attempts++; + try { + $callback = $this->callback; + $this->handleResult($callback()); + } catch (Exception $e) { + $this->handleResult($e); + } + } + + protected function logError(Exception $e) + { + if ($this->lastError !== $e->getMessage()) { + $this->lastError = $e->getMessage(); + Logger::error($e); + } + } + + protected function handleResult($result) + { + if ($this->expectsSuccess) { + if ($result instanceof Exception) { + $this->logError($result); + $this->scheduleNextAttempt(); + } elseif ($result instanceof PromiseInterface) { + $later = function ($result) { + $this->handleResult($result); + }; + $result->then($later, $later); + } else { + $this->succeed($result); + } + } else { + if ($result instanceof Exception) { + $this->succeed($result); + } else { + $this->scheduleNextAttempt(); + } + } + } + + protected function scheduleNextAttempt() + { + if ($this->timer !== null) { + throw new RuntimeException( + 'RetryUnless schedules next attempt while already scheduled' + ); + } + $this->timer = $this->loop->addTimer($this->getNextInterval(), function () { + $this->nextAttempt(); + }); + } + + protected function succeed($result) + { + $this->removeEventualTimer(); + if ($this->deferred === null) { + Logger::warning('RetryUnless tries to resolve twice'); + + return; + } + $this->deferred->resolve($result); + $this->deferred = null; + $this->reset(); + } + + protected function getNextInterval() + { + if ($this->burst === null) { + return $this->interval; + } + + return $this->attempts >= $this->burst + ? $this->finalInterval + : $this->interval; + } + + protected function assertNotRunning() + { + if ($this->deferred) { + throw new RuntimeException( + 'Cannot re-run RetryUnless while already running' + ); + } + } + + protected function removeEventualTimer() + { + if ($this->timer) { + $this->loop->cancelTimer($this->timer); + $this->timer = null; + } + } + + protected function rejectEventualDeferred($reason) + { + if ($this->deferred !== null) { + $deferred = $this->deferred; + $this->deferred = null; + $deferred->reject($reason); + } + } + + public function __destruct() + { + $this->removeEventualTimer(); + $this->rejectEventualDeferred('RetryUnless has been destructed'); + + $this->loop = null; + } +} diff --git a/vendor/gipfl/icinga-cli-daemon/src/StateMachine.php b/vendor/gipfl/icinga-cli-daemon/src/StateMachine.php new file mode 100644 index 0000000..d8ec470 --- /dev/null +++ b/vendor/gipfl/icinga-cli-daemon/src/StateMachine.php @@ -0,0 +1,113 @@ +<?php + +namespace gipfl\IcingaCliDaemon; + +use RuntimeException; + +trait StateMachine +{ + /** @var string */ + private $currentState; + + /** @var array [fromState][toState] = [callback, ...] */ + private $allowedTransitions = []; + + /** @var array [state] = [callback, ...] */ + private $onState = []; + + public function initializeStateMachine($initialState) + { + if ($this->currentState !== null) { + throw new RuntimeException('StateMachine has already been initialized'); + } + $this->currentState = $initialState; + } + + /** + * @param string|array $fromState + * @param string $toState + * @param callable $callback + * @return $this + */ + public function onTransition($fromState, $toState, $callback) + { + if (is_array($fromState)) { + foreach ($fromState as $state) { + $this->onTransition($state, $toState, $callback); + } + } else { + $this->allowTransition($fromState, $toState); + $this->allowedTransitions[$fromState][$toState][] = $callback; + } + + return $this; + } + + public function allowTransition($fromState, $toState) + { + if (! isset($this->allowedTransitions[$fromState][$toState])) { + $this->allowedTransitions[$fromState][$toState] = []; + } + + return $this; + } + + /** + * @param $state + * @param $callback + * @return $this + */ + public function onState($state, $callback) + { + if (! isset($this->onState[$state])) { + $this->onState[$state] = []; + } + + $this->onState[$state][] = $callback; + + return $this; + } + + public function getState() + { + if ($this->currentState === null) { + throw new RuntimeException('StateMachine has not been initialized'); + } + + return $this->currentState; + } + + public function setState($state) + { + $fromState = $this->getState(); + if ($this->canTransit($fromState, $state)) { + $this->currentState = $state; + $this->runStateTransition($fromState, $state); + } else { + throw new RuntimeException(sprintf( + 'A transition from %s to %s is not allowed', + $fromState, + $state + )); + } + } + + private function runStateTransition($fromState, $toState) + { + if (isset($this->allowedTransitions[$fromState][$toState])) { + foreach ($this->allowedTransitions[$fromState][$toState] as $callback) { + $callback(); + } + } + if (isset($this->onState[$toState])) { + foreach ($this->onState[$toState] as $callback) { + $callback(); + } + } + } + + public function canTransit($fromState, $toState) + { + return isset($this->allowedTransitions[$fromState][$toState]); + } +} diff --git a/vendor/gipfl/icingaweb2/composer.json b/vendor/gipfl/icingaweb2/composer.json new file mode 100644 index 0000000..25a757f --- /dev/null +++ b/vendor/gipfl/icingaweb2/composer.json @@ -0,0 +1,21 @@ +{ + "name": "gipfl/icingaweb2", + "type": "library", + "description": "Helpers and glue for Icinga Web 2", + "homepage": "https://github.com/gipfl/icingaweb2", + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\IcingaWeb2\\": "src/" + } + }, + "require": { + "php": ">=5.6", + "ext-ctype": "*", + "gipfl/format": ">=0.3", + "gipfl/translation": ">=0.1", + "ipl/html": ">=0.2.1" + } +} diff --git a/vendor/gipfl/icingaweb2/public/css/11-action-bar.less b/vendor/gipfl/icingaweb2/public/css/11-action-bar.less new file mode 100644 index 0000000..ab3d737 --- /dev/null +++ b/vendor/gipfl/icingaweb2/public/css/11-action-bar.less @@ -0,0 +1,105 @@ +div.gipfl-action-bar { + > * { + vertical-align: middle; + } + .pagination-control { + float: none; + clear: none; + display: inline-block; + line-height: inherit; + margin: 0; + } + + form input { + margin: 0; + } +} + +div.gipfl-action-bar a, div.gipfl-action-bar form i { + color: @icinga-blue; +} + +div.gipfl-action-bar a.badge { + color: inherit; +} + +div.gipfl-action-bar > a { + margin-right: 1em; +} + +#layout.twocols div.gipfl-action-bar .pagination-control { + li { + display: none; + } + + li:nth-child(1), li.active, li:last-child { + display: list-item; + } + + li.active a { + border-bottom: none; + } +} + +/** Dropdown in action bar **/ +div.gipfl-action-bar ul { + padding: 0; + margin: 0; + + li { + list-style-type: none; + a { display: block; } + } +} + +div.gipfl-action-bar > ul { + display: inline-block; + > li { + display: inline-block; + } + ul { + padding: 0.5em 1em 0 0; + min-width: 10em; + position: absolute; + display: none; + background-color: @menu-flyout-bg-color; + border: 1px solid @gray-light; + box-shadow: 0.3em 0.3em 0.3em 0 rgba(0, 0, 0, 0.2); + + a { + display: block; + padding: 0.3em 0.5em 0.3em 1em; + margin: 0; + outline: none; + color: @menu-flyout-color; + + &:hover { + text-decoration: underline; + &:before { + text-decoration: none; + } + } + } + + li.active a { + font-weight: bold; + } + } + > li:hover ul { + display: block; + z-index: 2; + } + > li > a { + padding: 0.2em 0.5em; + } + > li:hover { + background-color: @tr-active-color; + border-top-left-radius: 0.1em; + border-top-right-radius: 0.1em; + & > a { + color: @icinga-blue; + text-decoration: none; + } + } +} +/** END of dropdown in action bar **/ diff --git a/vendor/gipfl/icingaweb2/public/css/12-quicksearch.less b/vendor/gipfl/icingaweb2/public/css/12-quicksearch.less new file mode 100644 index 0000000..a5da7e9 --- /dev/null +++ b/vendor/gipfl/icingaweb2/public/css/12-quicksearch.less @@ -0,0 +1,17 @@ +form.gipfl-quicksearch { + display: block; + input.search { + width: 8em; + min-width: unset; + border: none; + background-color: inherit; + padding-left: 2em; + margin-left: 1.5em; + font-size: 0.75em; + font-weight: normal; + &:focus { + width: 16em; + border: none; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/CompatController.php b/vendor/gipfl/icingaweb2/src/CompatController.php new file mode 100644 index 0000000..952b4b5 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/CompatController.php @@ -0,0 +1,581 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use gipfl\IcingaWeb2\Controller\Extension\AutoRefreshHelper; +use gipfl\IcingaWeb2\Controller\Extension\ConfigHelper; +use gipfl\Translation\StaticTranslator; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Controller\Extension\ControlsAndContentHelper; +use gipfl\IcingaWeb2\Widget\ControlsAndContent; +use gipfl\IcingaWeb2\Zf1\SimpleViewRenderer; +use GuzzleHttp\Psr7\ServerRequest; +use Icinga\Application\Benchmark; +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Manager; +use Icinga\Application\Modules\Module; +use Icinga\Authentication\Auth; +use Icinga\Exception\ProgrammingError; +use Icinga\File\Pdf; +use Icinga\Security\SecurityException; +use Icinga\User; +use Icinga\Web\Notification; +use Icinga\Web\Session; +use Icinga\Web\UrlParams; +use Icinga\Web\Url as WebUrl; +use Icinga\Web\Window; +use InvalidArgumentException; +use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; +use Zend_Controller_Action; +use Zend_Controller_Action_HelperBroker as ZfHelperBroker; +use Zend_Controller_Request_Abstract as ZfRequest; +use Zend_Controller_Response_Abstract as ZfResponse; + +/** + * Class CompatController + * @method \Icinga\Web\Request getRequest() { + * {@inheritdoc} + * @return \Icinga\Web\Request + * } + * + * @method \Icinga\Web\Response getResponse() { + * {@inheritdoc} + * @return \Icinga\Web\Response + * } + */ +class CompatController extends Zend_Controller_Action implements ControlsAndContent +{ + use AutoRefreshHelper; + use ControlsAndContentHelper; + use ConfigHelper; + use TranslationHelper; + + /** @var bool Whether the controller requires the user to be authenticated */ + protected $requiresAuthentication = true; + + protected $applicationName = 'Icinga Web'; + + /** @var string The current module's name */ + private $moduleName; + + private $module; + + private $window; + + // https://github.com/joshbuchea/HEAD + + /** @var SimpleViewRenderer */ + protected $viewRenderer; + + /** @var bool */ + private $reloadCss = false; + + /** @var bool */ + private $rerenderLayout = false; + + /** @var string */ + private $xhrLayout = 'inline'; + + /** @var \Zend_Layout */ + private $layout; + + /** @var string The inner layout (inside the body) to use */ + private $innerLayout = 'body'; + + /** + * Authentication manager + * + * @var Auth|null + */ + private $auth; + + /** @var UrlParams */ + protected $params; + + /** + * The constructor starts benchmarking, loads the configuration and sets + * other useful controller properties + * + * @param ZfRequest $request + * @param ZfResponse $response + * @param array $invokeArgs Any additional invocation arguments + * @throws SecurityException + */ + public function __construct( + ZfRequest $request, + ZfResponse $response, + array $invokeArgs = array() + ) { + /** @var \Icinga\Web\Request $request */ + /** @var \Icinga\Web\Response $response */ + $this->setRequest($request) + ->setResponse($response) + ->_setInvokeArgs($invokeArgs); + + $this->prepareViewRenderer(); + $this->_helper = new ZfHelperBroker($this); + + $this->handlerBrowserWindows(); + $this->initializeTranslator(); + $this->initializeLayout(); + + $this->view->compact = $request->getParam('view') === 'compact'; + $url = $this->url(); + $this->params = $url->getParams(); + + if ($url->shift('showCompact')) { + $this->view->compact = true; + } + + $this->checkPermissionBasics(); + Benchmark::measure('Ready to initialize the controller'); + $this->prepareInit(); + $this->init(); + } + + protected function initializeLayout() + { + $url = $this->url(); + $layout = $this->layout = $this->_helper->layout(); + $layout->isIframe = $url->shift('isIframe'); + $layout->showFullscreen = $url->shift('showFullscreen'); + $layout->moduleName = $this->getModuleName(); + if ($this->rerenderLayout = $url->shift('renderLayout')) { + $this->xhrLayout = $this->innerLayout; + } + if ($url->shift('_disableLayout')) { + $this->layout->disableLayout(); + } + } + + /** + * Prepare controller initialization + * + * As it should not be required for controllers to call the parent's init() method, + * base controllers should use prepareInit() in order to prepare the controller + * initialization. + * + * @see \Zend_Controller_Action::init() For the controller initialization method. + */ + protected function prepareInit() + { + } + + /** + * Return the current module's name + * + * @return string + */ + public function getModuleName() + { + if ($this->moduleName === null) { + $this->moduleName = $this->getRequest()->getModuleName(); + } + + return $this->moduleName; + } + + protected function setModuleName($name) + { + $this->moduleName = $name; + + return $this; + } + + /** + * Return this controller's module + * + * @return Module + * @codingStandardsIgnoreStart + */ + public function Module() + { + // @codingStandardsIgnoreEnd + if ($this->module === null) { + try { + $this->module = Icinga::app()->getModuleManager()->getModule($this->getModuleName()); + } catch (ProgrammingError $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } + + return $this->module; + } + + /** + * @return Window + * @codingStandardsIgnoreStart + */ + public function Window() + { + // @codingStandardsIgnoreEnd + if ($this->window === null) { + $this->window = new Window( + $this->getRequestHeader('X-Icinga-WindowId', Window::UNDEFINED) + ); + } + return $this->window; + } + + protected function handlerBrowserWindows() + { + if ($this->isXhr()) { + $id = $this->getRequestHeader('X-Icinga-WindowId', Window::UNDEFINED); + + if ($id === Window::UNDEFINED) { + $this->window = new Window($id); + $this->_response->setHeader('X-Icinga-WindowId', Window::generateId()); + } + } + } + + /** + * @return ServerRequestInterface + */ + protected function getServerRequest() + { + return ServerRequest::fromGlobals(); + } + + protected function getRequestHeader($key, $default = null) + { + try { + $value = $this->getRequest()->getHeader($key); + } catch (\Zend_Controller_Request_Exception $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + if ($value === null) { + return $default; + } else { + return $value; + } + } + + /** + * @throws SecurityException + */ + protected function checkPermissionBasics() + { + $request = $this->getRequest(); + // $auth->authenticate($request, $response, $this->requiresLogin()); + if ($this->requiresLogin()) { + if (! $request->isXmlHttpRequest() && $request->isApiRequest()) { + Auth::getInstance()->challengeHttp(); + } + $this->redirectToLogin(Url::fromRequest()); + } + if (($this->Auth()->isAuthenticated() || $this->requiresLogin()) + && $this->getFrontController()->getDefaultModule() !== $this->getModuleName()) { + $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->getModuleName()); + } + } + + protected function initializeTranslator() + { + $moduleName = $this->getModuleName(); + $domain = $moduleName !== 'default' ? $moduleName : 'icinga'; + $this->view->translationDomain = $domain; + StaticTranslator::set(new Translator($domain)); + } + + public function init() + { + // Hint: we intentionally do not call our parent's init() method + } + + /** + * Get the authentication manager + * + * @return Auth + * @codingStandardsIgnoreStart + */ + public function Auth() + { + // @codingStandardsIgnoreEnd + if ($this->auth === null) { + $this->auth = Auth::getInstance(); + } + return $this->auth; + } + + /** + * Whether the current user has the given permission + * + * @param string $permission Name of the permission + * + * @return bool + */ + public function hasPermission($permission) + { + return $this->Auth()->hasPermission($permission); + } + + /** + * Assert that the current user has the given permission + * + * @param string $permission Name of the permission + * + * @throws SecurityException If the current user lacks the given permission + */ + public function assertPermission($permission) + { + if (! $this->Auth()->hasPermission($permission)) { + throw new SecurityException('No permission for %s', $permission); + } + } + + /** + * Return restriction information for an eventually authenticated user + * + * @param string $name Restriction name + * + * @return array + */ + public function getRestrictions($name) + { + return $this->Auth()->getRestrictions($name); + } + + /** + * Check whether the controller requires a login. That is when the controller requires authentication and the + * user is currently not authenticated + * + * @return bool + */ + protected function requiresLogin() + { + if (! $this->requiresAuthentication) { + return false; + } + + return ! $this->Auth()->isAuthenticated(); + } + + public function prepareViewRenderer() + { + $this->viewRenderer = new SimpleViewRenderer(); + $this->viewRenderer->replaceZendViewRenderer(); + $this->view = $this->viewRenderer->view; + } + + /** + * @return SimpleViewRenderer + */ + public function getViewRenderer() + { + return $this->viewRenderer; + } + + protected function redirectXhr($url) + { + if (! $url instanceof WebUrl) { + $url = Url::fromPath($url); + } + + if ($this->rerenderLayout) { + $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes'); + } + if ($this->reloadCss) { + $this->getResponse()->setHeader('X-Icinga-Reload-Css', 'now'); + } + + $this->shutdownSession(); + + $this->getResponse() + ->setHeader('X-Icinga-Redirect', rawurlencode($url->getAbsoluteUrl())) + ->sendHeaders(); + + exit; + } + + /** + * Detect whether the current request requires changes in the layout and apply them before rendering + * + * @see Zend_Controller_Action::postDispatch() + */ + public function postDispatch() + { + Benchmark::measure('Action::postDispatch()'); + + $layout = $this->layout; + $req = $this->getRequest(); + $layout->innerLayout = $this->innerLayout; + + /** @var User $user */ + if ($user = $req->getUser()) { + if ((bool) $user->getPreferences()->getValue('icingaweb', 'show_benchmark', false)) { + if ($layout->isEnabled()) { + $layout->benchmark = $this->renderBenchmark(); + } + } + + if (! (bool) $user->getPreferences()->getValue('icingaweb', 'auto_refresh', true)) { + $this->disableAutoRefresh(); + } + } + + if ($req->getParam('format') === 'pdf') { + $this->shutdownSession(); + $this->sendAsPdf(); + return; + } + + if ($this->isXhr()) { + $this->postDispatchXhr(); + } + + $this->shutdownSession(); + } + + public function postDispatchXhr() + { + $this->layout->setLayout($this->xhrLayout); + $resp = $this->getResponse(); + + $notifications = Notification::getInstance(); + if ($notifications->hasMessages()) { + $notificationList = array(); + foreach ($notifications->popMessages() as $m) { + $notificationList[] = rawurlencode($m->type . ' ' . $m->message); + } + $resp->setHeader('X-Icinga-Notification', implode('&', $notificationList), true); + } + + if ($this->reloadCss) { + $resp->setHeader('X-Icinga-CssReload', 'now', true); + } + + if ($this->title) { + if (preg_match('~[\r\n]~', $this->title)) { + // TODO: Innocent exception and error log for hack attempts + throw new InvalidArgumentException('No newlines allowed in page title'); + } + $resp->setHeader( + 'X-Icinga-Title', + // TODO: Config + rawurlencode($this->title . ' :: ' . $this->applicationName), + true + ); + } else { + // TODO: config + $resp->setHeader('X-Icinga-Title', rawurlencode($this->applicationName), true); + } + + if ($this->rerenderLayout) { + $this->getResponse()->setHeader('X-Icinga-Container', 'layout', true); + } + + if (isset($this->autorefreshInterval)) { + $resp->setHeader('X-Icinga-Refresh', $this->autorefreshInterval, true); + } + + if ($name = $this->getModuleName()) { + $this->getResponse()->setHeader('X-Icinga-Module', $name, true); + } + } + + /** + * Redirect to login + * + * XHR will always redirect to __SELF__ if an URL to redirect to after successful login is set. __SELF__ instructs + * JavaScript to redirect to the current window's URL if it's an auto-refresh request or to redirect to the URL + * which required login if it's not an auto-refreshing one. + * + * XHR will respond with HTTP status code 403 Forbidden. + * + * @param Url|string $redirect URL to redirect to after successful login + */ + protected function redirectToLogin($redirect = null) + { + $login = Url::fromPath('authentication/login'); + if ($this->isXhr()) { + if ($redirect !== null) { + $login->setParam('redirect', '__SELF__'); + } + + $this->_response->setHttpResponseCode(403); + } elseif ($redirect !== null) { + if (! $redirect instanceof Url) { + $redirect = Url::fromPath($redirect); + } + + if (($relativeUrl = $redirect->getRelativeUrl())) { + $login->setParam('redirect', $relativeUrl); + } + } + + $this->rerenderLayout()->redirectNow($login); + } + + protected function sendAsPdf() + { + $pdf = new Pdf(); + $pdf->renderControllerAction($this); + } + + /** + * Render the benchmark + * + * @return string Benchmark HTML + */ + protected function renderBenchmark() + { + $this->viewRenderer->postDispatch(); + Benchmark::measure('Response ready'); + return Benchmark::renderToHtml()/* + . '<pre style="height: 16em; vertical-overflow: scroll">' + . print_r(get_included_files(), 1) + . '</pre>'*/; + } + + public function isXhr() + { + return $this->getRequest()->isXmlHttpRequest(); + } + + protected function redirectHttp($url) + { + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + $this->shutdownSession(); + $this->_helper->Redirector->gotoUrlAndExit($url->getRelativeUrl()); + } + + /** + * Redirect to a specific url, updating the browsers URL field + * + * @param Url|string $url The target to redirect to + **/ + public function redirectNow($url) + { + if ($this->isXhr()) { + $this->redirectXhr($url); + } else { + $this->redirectHttp($url); + } + } + + protected function shutdownSession() + { + $session = Session::getSession(); + if ($session->hasChanged()) { + $session->write(); + } + } + + protected function rerenderLayout() + { + $this->rerenderLayout = true; + $this->xhrLayout = 'layout'; + return $this; + } + + protected function reloadCss() + { + $this->reloadCss = true; + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Controller/Extension/AutoRefreshHelper.php b/vendor/gipfl/icingaweb2/src/Controller/Extension/AutoRefreshHelper.php new file mode 100644 index 0000000..5b899ba --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Controller/Extension/AutoRefreshHelper.php @@ -0,0 +1,30 @@ +<?php + +namespace gipfl\IcingaWeb2\Controller\Extension; + +use InvalidArgumentException; + +trait AutoRefreshHelper +{ + /** @var int|null */ + private $autorefreshInterval; + + public function setAutorefreshInterval($interval) + { + if (! is_int($interval) || $interval < 1) { + throw new InvalidArgumentException( + 'Setting autorefresh interval smaller than 1 second is not allowed' + ); + } + $this->autorefreshInterval = $interval; + $this->layout->autorefreshInterval = $interval; + return $this; + } + + public function disableAutoRefresh() + { + $this->autorefreshInterval = null; + $this->layout->autorefreshInterval = null; + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Controller/Extension/ConfigHelper.php b/vendor/gipfl/icingaweb2/src/Controller/Extension/ConfigHelper.php new file mode 100644 index 0000000..72cefa7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Controller/Extension/ConfigHelper.php @@ -0,0 +1,46 @@ +<?php + +namespace gipfl\IcingaWeb2\Controller\Extension; + +use Icinga\Application\Config; + +trait ConfigHelper +{ + private $config; + + private $configs = []; + + /** + * @param null $file + * @return Config + * @codingStandardsIgnoreStart + */ + public function Config($file = null) + { + // @codingStandardsIgnoreEnd + if ($this->moduleName === null) { + if ($file === null) { + return Config::app(); + } else { + return Config::app($file); + } + } else { + return $this->getModuleConfig($file); + } + } + + public function getModuleConfig($file = null) + { + if ($file === null) { + if ($this->config === null) { + $this->config = Config::module($this->getModuleName()); + } + return $this->config; + } else { + if (! array_key_exists($file, $this->configs)) { + $this->configs[$file] = Config::module($this->getModuleName(), $file); + } + return $this->configs[$file]; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Controller/Extension/ControlsAndContentHelper.php b/vendor/gipfl/icingaweb2/src/Controller/Extension/ControlsAndContentHelper.php new file mode 100644 index 0000000..00ef389 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Controller/Extension/ControlsAndContentHelper.php @@ -0,0 +1,173 @@ +<?php + +namespace gipfl\IcingaWeb2\Controller\Extension; + +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\Content; +use gipfl\IcingaWeb2\Widget\Controls; +use gipfl\IcingaWeb2\Widget\Tabs; +use ipl\Html\HtmlDocument; + +trait ControlsAndContentHelper +{ + /** @var Controls */ + private $controls; + + /** @var Content */ + private $content; + + protected $title; + + /** @var Url */ + private $url; + + /** @var Url */ + private $originalUrl; + + /** + * TODO: Not sure whether we need dedicated Content/Controls classes, + * a simple Container with a class name might suffice here + * + * @return Controls + */ + public function controls() + { + if ($this->controls === null) { + $this->view->controls = $this->controls = new Controls(); + } + + return $this->controls; + } + + /** + * @param Tabs|null $tabs + * @return Tabs + */ + public function tabs(Tabs $tabs = null) + { + if ($tabs === null) { + return $this->controls()->getTabs(); + } else { + $this->controls()->setTabs($tabs); + return $tabs; + } + } + + /** + * @param HtmlDocument|null $actionBar + * @return HtmlDocument + */ + public function actions(HtmlDocument $actionBar = null) + { + if ($actionBar === null) { + return $this->controls()->getActionBar(); + } else { + $this->controls()->setActionBar($actionBar); + return $actionBar; + } + } + + /** + * @return Content + */ + public function content() + { + if ($this->content === null) { + $this->view->content = $this->content = new Content(); + } + + return $this->content; + } + + /** + * @param $title + * @return $this + */ + public function setTitle($title) + { + $this->title = $this->makeTitle(func_get_args()); + return $this; + } + + /** + * @param $title + * @return $this + */ + public function addTitle($title) + { + $title = $this->makeTitle(func_get_args()); + $this->title = $title; + $this->controls()->addTitle($title); + + return $this; + } + + private function makeTitle($args) + { + $title = array_shift($args); + + if (empty($args)) { + return $title; + } else { + return vsprintf($title, $args); + } + } + + /** + * @param string $title + * @param mixed $url + * @param string $name + * @return $this + */ + public function addSingleTab($title, $url = null, $name = 'main') + { + if ($url === null) { + $url = $this->url(); + } + + $this->tabs()->add($name, [ + 'label' => $title, + 'url' => $url, + ])->activate($name); + + return $this; + } + + /** + * @return Url + */ + public function url() + { + if ($this->url === null) { + $this->url = $this->getOriginalUrl(); + } + + return $this->url; + } + + /** + * @return Url + */ + public function getOriginalUrl() + { + if ($this->originalUrl === null) { + $this->originalUrl = clone($this->getUrlFromRequest()); + } + + return clone($this->originalUrl); + } + + /** + * @return Url + */ + protected function getUrlFromRequest() + { + /** @var \Icinga\Web\Request $request */ + $request = $this->getRequest(); + $webUrl = $request->getUrl(); + + return Url::fromPath( + $webUrl->getPath() + )->setParams($webUrl->getParams()); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Data/Paginatable.php b/vendor/gipfl/icingaweb2/src/Data/Paginatable.php new file mode 100644 index 0000000..8cd3963 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Data/Paginatable.php @@ -0,0 +1,64 @@ +<?php + +namespace gipfl\IcingaWeb2\Data; + +use Countable; + +interface Paginatable extends Countable +{ + /** + * Set a limit count and offset + * + * @param int $count Number of rows to return + * @param int $offset Skip that many rows + * + * @return self + */ + public function limit($count = null, $offset = null); + + /** + * Whether a limit is set + * + * @return bool + */ + public function hasLimit(); + + /** + * Get the limit if any + * + * @return int|null + */ + public function getLimit(); + + /** + * Set limit + * + * @param int $limit Number of rows to return + * + * @return int|null + */ + public function setLimit($limit); + + /** + * Whether an offset is set + * + * @return bool + */ + public function hasOffset(); + + /** + * Get the offset if any + * + * @return int|null + */ + public function getOffset(); + + /** + * Set offset + * + * @param int $offset Skip that many rows + * + * @return int|null + */ + public function setOffset($offset); +} diff --git a/vendor/gipfl/icingaweb2/src/Data/SimpleQueryPaginationAdapter.php b/vendor/gipfl/icingaweb2/src/Data/SimpleQueryPaginationAdapter.php new file mode 100644 index 0000000..d6a1b97 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Data/SimpleQueryPaginationAdapter.php @@ -0,0 +1,68 @@ +<?php + +namespace gipfl\IcingaWeb2\Data; + +use Icinga\Application\Benchmark; +use Icinga\Data\SimpleQuery; + +class SimpleQueryPaginationAdapter implements Paginatable +{ + /** @var SimpleQuery */ + private $query; + + public function __construct(SimpleQuery $query) + { + $this->query = $query; + } + + #[\ReturnTypeWillChange] + public function count() + { + Benchmark::measure('Running count() for pagination'); + $count = $this->query->count(); + Benchmark::measure("Counted $count rows"); + + return $count; + } + + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + } + + public function hasLimit() + { + return $this->getLimit() !== null; + } + + public function getLimit() + { + return $this->query->getLimit(); + } + + public function setLimit($limit) + { + $this->query->limit( + $limit, + $this->getOffset() + ); + } + + public function hasOffset() + { + return $this->getOffset() !== null; + } + + public function getOffset() + { + return $this->query->getOffset(); + } + + public function setOffset($offset) + { + $this->query->limit( + $this->getLimit(), + $offset + ); + } +} diff --git a/vendor/gipfl/icingaweb2/src/FakeRequest.php b/vendor/gipfl/icingaweb2/src/FakeRequest.php new file mode 100644 index 0000000..854c26e --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/FakeRequest.php @@ -0,0 +1,32 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Icinga\Web\Request; +use RuntimeException; + +class FakeRequest extends Request +{ + /** @var string */ + private static $baseUrl; + + public static function setConfiguredBaseUrl($url) + { + self::$baseUrl = $url; + } + + public function setUrl(Url $url) + { + $this->url = $url; + return $this; + } + + public function getBaseUrl($raw = false) + { + if (self::$baseUrl === null) { + throw new RuntimeException('Cannot determine base URL on CLI if not configured'); + } else { + return self::$baseUrl; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Icon.php b/vendor/gipfl/icingaweb2/src/Icon.php new file mode 100644 index 0000000..0001e12 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Icon.php @@ -0,0 +1,27 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use ipl\Html\BaseHtmlElement; + +class Icon extends BaseHtmlElement +{ + protected $tag = 'i'; + + public function __construct($name, $attributes = null) + { + $this->setAttributes($attributes); + $this->getAttributes()->add('class', array('icon', 'icon-' . $name)); + } + + /** + * @param string $name + * @param array $attributes + * + * @return static + */ + public static function create($name, array $attributes = null) + { + return new static($name, $attributes); + } +} diff --git a/vendor/gipfl/icingaweb2/src/IconHelper.php b/vendor/gipfl/icingaweb2/src/IconHelper.php new file mode 100644 index 0000000..1c8af9d --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/IconHelper.php @@ -0,0 +1,89 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use InvalidArgumentException; + +/** + * Icon helper class + * + * Should help to reduce redundant icon-lookup code. Currently with hardcoded + * icons only, could easily provide support for all of them as follows: + * + * $confFile = Icinga::app() + * ->getApplicationDir('fonts/fontello-ifont/config.json'); + * + * $font = json_decode(file_get_contents($confFile)); + * // 'icon-' is to be found in $font->css_prefix_text + * foreach ($font->glyphs as $icon) { + * // $icon->css (= 'help') -> 0x . dechex($icon->code) + * } + */ +class IconHelper +{ + private $icons = [ + 'minus' => 'e806', + 'trash' => 'e846', + 'plus' => 'e805', + 'cancel' => 'e804', + 'help' => 'e85b', + 'angle-double-right' => 'e87b', + 'up-big' => 'e825', + 'down-big' => 'e828', + 'down-open' => 'e821', + ]; + + private $mappedUtf8Icons; + + private $reversedUtf8Icons; + + private static $instance; + + public function __construct() + { + $this->prepareIconMappings(); + } + + public static function instance() + { + if (self::$instance === null) { + self::$instance = new static; + } + + return self::$instance; + } + + public function characterIconName($character) + { + if (array_key_exists($character, $this->reversedUtf8Icons)) { + return $this->reversedUtf8Icons[$character]; + } else { + throw new InvalidArgumentException('There is no mapping for the given character'); + } + } + + protected function hexToCharacter($hex) + { + return json_decode('"\u' . $hex . '"'); + } + + public function iconCharacter($name) + { + if (array_key_exists($name, $this->mappedUtf8Icons)) { + return $this->mappedUtf8Icons[$name]; + } else { + return $this->mappedUtf8Icons['help']; + } + } + + protected function prepareIconMappings() + { + $this->mappedUtf8Icons = []; + $this->reversedUtf8Icons = []; + foreach ($this->icons as $name => $hex) { + $character = $this->hexToCharacter($hex); + $this->mappedUtf8Icons[$name] = $character; + $this->reversedUtf8Icons[$character] = $name; + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Img.php b/vendor/gipfl/icingaweb2/src/Img.php new file mode 100644 index 0000000..3c68adb --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Img.php @@ -0,0 +1,83 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Icinga\Web\Url as WebUrl; +use ipl\Html\Attribute; +use ipl\Html\BaseHtmlElement; + +class Img extends BaseHtmlElement +{ + protected $tag = 'img'; + + /** @var Url */ + protected $url; + + protected $defaultAttributes = array('alt' => ''); + + protected function __construct() + { + } + + /** + * @param Url|string $url + * @param array $urlParams + * @param array $attributes + * + * @return static + */ + public static function create($url, $urlParams = null, array $attributes = null) + { + /** @var Img $img */ + $img = new static(); + $img->setAttributes($attributes); + $img->getAttributes()->registerAttributeCallback('src', array($img, 'getSrcAttribute')); + $img->setUrl($url, $urlParams); + return $img; + } + + public function setUrl($url, $urlParams) + { + if ($url instanceof WebUrl) { // Hint: Url is also a WebUrl + if ($urlParams !== null) { + $url->addParams($urlParams); + } + + $this->url = $url; + } else { + if ($urlParams === null) { + if (is_string($url) && substr($url, 0, 5) === 'data:') { + $this->url = $url; + return; + } else { + $this->url = Url::fromPath($url); + } + } else { + $this->url = Url::fromPath($url, $urlParams); + } + } + + $this->url->getParams(); + } + + /** + * @return Attribute + */ + public function getSrcAttribute() + { + if (is_string($this->url)) { + return new Attribute('src', $this->url); + } else { + return new Attribute('src', $this->getUrl()->getAbsoluteUrl('&')); + } + } + + /** + * @return Url + */ + public function getUrl() + { + // TODO: What if null? #? + return $this->url; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Link.php b/vendor/gipfl/icingaweb2/src/Link.php new file mode 100644 index 0000000..e6e4de9 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Link.php @@ -0,0 +1,85 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Icinga\Web\Url as WebUrl; +use ipl\Html\Attribute; +use ipl\Html\BaseHtmlElement; +use ipl\Html\ValidHtml; + +class Link extends BaseHtmlElement +{ + protected $tag = 'a'; + + /** @var Url */ + protected $url; + + /** + * Link constructor. + * @param $content + * @param $url + * @param null $urlParams + * @param array|null $attributes + */ + public function __construct($content, $url, $urlParams = null, array $attributes = null) + { + $this->setContent($content); + $this->setAttributes($attributes); + $this->getAttributes()->registerAttributeCallback('href', array($this, 'getHrefAttribute')); + $this->setUrl($url, $urlParams); + } + + /** + * @param ValidHtml|array|string $content + * @param Url|string $url + * @param array $urlParams + * @param mixed $attributes + * + * @return static + */ + public static function create($content, $url, $urlParams = null, array $attributes = null) + { + $link = new static($content, $url, $urlParams, $attributes); + return $link; + } + + /** + * @param $url + * @param $urlParams + */ + public function setUrl($url, $urlParams) + { + if ($url instanceof WebUrl) { // Hint: Url is also a WebUrl + if ($urlParams !== null) { + $url->addParams($urlParams); + } + + $this->url = $url; + } else { + if ($urlParams === null) { + $this->url = Url::fromPath($url); + } else { + $this->url = Url::fromPath($url, $urlParams); + } + } + + $this->url->getParams(); + } + + /** + * @return Attribute + */ + public function getHrefAttribute() + { + return new Attribute('href', $this->getUrl()->getAbsoluteUrl('&')); + } + + /** + * @return Url + */ + public function getUrl() + { + // TODO: What if null? #? + return $this->url; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/Extension/MultiSelect.php b/vendor/gipfl/icingaweb2/src/Table/Extension/MultiSelect.php new file mode 100644 index 0000000..7a5a3ff --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/Extension/MultiSelect.php @@ -0,0 +1,29 @@ +<?php + +namespace gipfl\IcingaWeb2\Table\Extension; + +use gipfl\IcingaWeb2\Url; + +// Could also be a static method, MultiSelect::enable($table) +trait MultiSelect +{ + protected function enableMultiSelect($url, $sourceUrl, array $keys) + { + /** @var $table \ipl\Html\BaseHtmlElement */ + $table = $this; + $table->addAttributes([ + 'class' => 'multiselect' + ]); + + $prefix = 'data-icinga-multiselect'; + $multi = [ + "$prefix-url" => Url::fromPath($url), + "$prefix-controllers" => Url::fromPath($sourceUrl), + "$prefix-data" => implode(',', $keys), + ]; + + $table->addAttributes($multi); + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/Extension/QuickSearch.php b/vendor/gipfl/icingaweb2/src/Table/Extension/QuickSearch.php new file mode 100644 index 0000000..0f0cec3 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/Extension/QuickSearch.php @@ -0,0 +1,74 @@ +<?php + +namespace gipfl\IcingaWeb2\Table\Extension; + +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\Controls; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +trait QuickSearch +{ + /** @var BaseHtmlElement */ + private $quickSearchForm; + + public function getQuickSearch(BaseHtmlElement $parent, Url $url) + { + $this->requireQuickSearchForm($parent, $url); + $search = $url->getParam('q'); + return $search; + } + + private function requireQuickSearchForm(BaseHtmlElement $parent, Url $url) + { + if ($this->quickSearchForm === null) { + $this->quickSearchForm = $this->buildQuickSearchForm($parent, $url); + } + } + + private function buildQuickSearchForm(BaseHtmlElement $parent, Url $url) + { + $search = $url->getParam('q'); + + $form = Html::tag('form', [ + 'action' => $url->without(array('q', 'page', 'modifyFilter'))->getAbsoluteUrl(), + 'class' => ['gipfl-quicksearch'], + 'method' => 'GET' + ]); + + $form->add( + Html::tag('input', [ + 'type' => 'text', + 'name' => 'q', + 'title' => $this->translate('Search is simple! Try to combine multiple words'), + 'value' => $search, + 'placeholder' => $this->translate('Search...'), + 'class' => 'search' + ]) + ); + + $this->addQuickSearchToControls($parent, $form); + + return $form; + } + + protected function addQuickSearchToControls(BaseHtmlElement $parent, BaseHtmlElement $form) + { + if ($parent instanceof Controls) { + $title = $parent->getTitleElement(); + if ($title === null) { + $parent->prepend($form); + } else { + $input = $form->getFirst('input'); + $form->remove($input); + $title->add($input); + $form->add($title); + $parent->setTitleElement($form); + } + } else { + $parent->prepend($form); + } + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/Extension/ZfSortablePriority.php b/vendor/gipfl/icingaweb2/src/Table/Extension/ZfSortablePriority.php new file mode 100644 index 0000000..cb1eac6 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/Extension/ZfSortablePriority.php @@ -0,0 +1,263 @@ +<?php + +namespace gipfl\IcingaWeb2\Table\Extension; + +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\IconHelper; +use gipfl\ZfDb\Exception\SelectException; +use gipfl\ZfDb\Select; +use Icinga\Web\Request; +use Icinga\Web\Response; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use RuntimeException; +use Zend_Db_Select_Exception as ZfDbSelectException; + +/** + * Trait ZfSortablePriority + * + * Assumes to run in a ZfQueryBasedTable + */ +trait ZfSortablePriority +{ + /** @var Request */ + protected $request; + + /** @var Response */ + protected $response; + + public function handleSortPriorityActions(Request $request, Response $response) + { + $this->request = $request; + $this->response = $response; + return $this; + } + + protected function reallyHandleSortPriorityActions() + { + $request = $this->request; + + if ($request->isPost() && $this->hasBeenSent($request)) { + // $this->fixPriorities(); + foreach (array_keys($request->getPost()) as $key) { + if (substr($key, 0, 8) === 'MOVE_UP_') { + $id = (int) substr($key, 8); + $this->moveRow($id, 'up'); + } + if (substr($key, 0, 10) === 'MOVE_DOWN_') { + $id = (int) substr($key, 10); + $this->moveRow($id, 'down'); + } + } + $this->response->redirectAndExit($request->getUrl()); + } + } + + protected function hasBeenSent(Request $request) + { + return $request->getPost('__FORM_NAME') === $this->getUniqueFormName(); + } + + protected function addSortPriorityButtons(BaseHtmlElement $tr, $row) + { + $tr->add( + Html::tag( + 'td', + null, + $this->createUpDownButtons($row->{$this->getKeyColumn()}) + ) + ); + + return $tr; + } + + protected function getKeyColumn() + { + if (isset($this->keyColumn)) { + return $this->keyColumn; + } else { + throw new RuntimeException( + 'ZfSortablePriority requires keyColumn' + ); + } + } + + protected function getPriorityColumn() + { + if (isset($this->priorityColumn)) { + return $this->priorityColumn; + } else { + throw new RuntimeException( + 'ZfSortablePriority requires priorityColumn' + ); + } + } + + protected function getPriorityColumns() + { + return [ + 'id' => $this->getKeyColumn(), + 'prio' => $this->getPriorityColumn() + ]; + } + + protected function moveRow($id, $direction) + { + /** @var $this ZfQueryBasedTable */ + $db = $this->db(); + /** @var $this ZfQueryBasedTable */ + $query = $this->getQuery(); + $tableParts = $this->getQueryPart(Select::FROM); + $alias = key($tableParts); + $table = $tableParts[$alias]['tableName']; + + $whereParts = $this->getQueryPart(Select::WHERE); + unset($query); + if (empty($whereParts)) { + $where = ''; + } else { + $where = ' AND ' . implode(' ', $whereParts); + } + + $prioCol = $this->getPriorityColumn(); + $keyCol = $this->getKeyColumn(); + $myPrio = (int) $db->fetchOne( + $db->select() + ->from($table, $prioCol) + ->where("$keyCol = ?", $id) + ); + + $op = $direction === 'up' ? '<' : '>'; + $sortDir = $direction === 'up' ? 'DESC' : 'ASC'; + $query = $db->select() + ->from([$alias => $table], $this->getPriorityColumns()) + ->where("$prioCol $op ?", $myPrio) + ->order("$prioCol $sortDir") + ->limit(1); + + if (! empty($whereParts)) { + $query->where(implode(' ', $whereParts)); + } + + $next = $db->fetchRow($query); + + if ($next) { + $sql = 'UPDATE %s %s' + . ' SET %s = CASE WHEN %s = %s THEN %d ELSE %d END' + . ' WHERE %s IN (%s, %s)'; + + $query = sprintf( + $sql, + $table, + $alias, + $prioCol, + $keyCol, + $id, + (int) $next->prio, + $myPrio, + $keyCol, + $id, + (int) $next->id + ) . $where; + + $db->query($query); + } + } + + protected function getSortPriorityTitle() + { + /** @var ZfQueryBasedTable $table */ + $table = $this; + + return Html::tag( + 'span', + ['title' => $table->translate('Change priority')], + $table->translate('Prio') + ); + } + + protected function createUpDownButtons($key) + { + /** @var ZfQueryBasedTable $table */ + $table = $this; + $up = $this->createIconButton( + "MOVE_UP_$key", + 'up-big', + $table->translate('Move up (raise priority)') + ); + $down = $this->createIconButton( + "MOVE_DOWN_$key", + 'down-big', + $table->translate('Move down (lower priority)') + ); + + if ($table->isOnFirstRow()) { + $up->getAttributes()->add('disabled', 'disabled'); + } + + if ($table->isOnLastRow()) { + $down->getAttributes()->add('disabled', 'disabled'); + } + + return [$down, $up]; + } + + protected function createIconButton($key, $icon, $title) + { + return Html::tag('input', [ + 'type' => 'submit', + 'class' => 'icon-button', + 'name' => $key, + 'title' => $title, + 'value' => IconHelper::instance()->iconCharacter($icon) + ]); + } + + protected function getUniqueFormName() + { + $parts = explode('\\', get_class($this)); + return end($parts); + } + + protected function renderWithSortableForm() + { + if ($this->request === null) { + return parent::render(); + } + $this->reallyHandleSortPriorityActions(); + + $url = $this->request->getUrl(); + // TODO: No margin for form + $form = Html::tag('form', [ + 'action' => $url->getAbsoluteUrl(), + 'method' => 'POST' + ], [ + Html::tag('input', [ + 'type' => 'hidden', + 'name' => '__FORM_NAME', + 'value' => $this->getUniqueFormName() + ]), + new HtmlString(parent::render()) + ]); + + return $form->render(); + } + + protected function getQueryPart($part) + { + /** @var ZfQueryBasedTable $table */ + $table = $this; + /** @var Select|\Zend_Db_Select $query */ + $query = $table->getQuery(); + try { + return $query->getPart($part); + } catch (SelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } catch (ZfDbSelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/QueryBasedTable.php b/vendor/gipfl/icingaweb2/src/Table/QueryBasedTable.php new file mode 100644 index 0000000..e9281c7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/QueryBasedTable.php @@ -0,0 +1,281 @@ +<?php + +namespace gipfl\IcingaWeb2\Table; + +use Countable; +use gipfl\Format\LocalDateFormat; +use gipfl\IcingaWeb2\Data\Paginatable; +use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer; +use gipfl\IcingaWeb2\Table\Extension\QuickSearch; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\ControlsAndContent; +use gipfl\IcingaWeb2\Widget\Paginator; +use gipfl\Translation\TranslationHelper; +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use ipl\Html\Table; + +abstract class QueryBasedTable extends Table implements Countable +{ + use TranslationHelper; + use QuickSearch; + + protected $defaultAttributes = [ + 'class' => ['common-table', 'table-row-selectable'], + 'data-base-target' => '_next', + ]; + + private $fetchedRows; + + private $firstRow; + + private $lastRow = false; + + private $rowNumber; + + private $rowNumberOnPage; + + protected $lastDay; + + /** @var Paginator|null Will usually be defined at rendering time */ + protected $paginator; + + private $isUsEnglish; + + private $dateFormatter; + + protected $searchColumns = []; + + /** + * @return Paginatable + */ + abstract protected function getPaginationAdapter(); + + abstract public function getQuery(); + + public function getPaginator(Url $url) + { + return new Paginator( + $this->getPaginationAdapter(), + $url + ); + } + + public function count() + { + return $this->getPaginationAdapter()->count(); + } + + public function applyFilter(Filter $filter) + { + FilterRenderer::applyToQuery($filter, $this->getQuery()); + return $this; + } + + protected function getSearchColumns() + { + return $this->searchColumns; + } + + public function search($search) + { + if (! empty($search)) { + $query = $this->getQuery(); + $columns = $this->getSearchColumns(); + if (strpos($search, ' ') === false) { + $filter = Filter::matchAny(); + foreach ($columns as $column) { + $filter->addFilter(Filter::expression($column, '=', "*$search*")); + } + } else { + $filter = Filter::matchAll(); + foreach (explode(' ', $search) as $s) { + $sub = Filter::matchAny(); + foreach ($columns as $column) { + $sub->addFilter(Filter::expression($column, '=', "*$s*")); + } + $filter->addFilter($sub); + } + } + + FilterRenderer::applyToQuery($filter, $query); + } + + return $this; + } + + abstract protected function prepareQuery(); + + public function renderContent() + { + $titleColumns = $this->renderTitleColumns(); + if ($titleColumns) { + $this->getHeader()->add($titleColumns); + } + $this->fetchRows(); + + return parent::renderContent(); + } + + protected function renderTitleColumns() + { + // TODO: drop this + if (method_exists($this, 'getColumnsToBeRendered')) { + $columns = $this->getColumnsToBeRendered(); + if (isset($columns) && count($columns)) { + return static::row($columns, null, 'th'); + } + } + + return null; + } + + protected function splitByDay($timestamp) + { + $this->renderDayIfNew((int) $timestamp); + } + + public function isOnFirstPage() + { + if ($this->paginator === null) { + // No paginator? Then there should be only a single page + return true; + } + + return $this->paginator->getCurrentPage() === 1; + } + + public function isOnFirstRow() + { + return $this->firstRow === true; + } + + public function isOnLastRow() + { + return $this->lastRow === true; + } + + protected function fetchRows() + { + $firstPage = $this->isOnFirstPage(); + $this->rowNumberOnPage = 0; + $this->rowNumber = $this->getPaginationAdapter()->getOffset(); + $lastRow = count($this); + foreach ($this->fetch() as $row) { + $this->rowNumber++; + $this->rowNumberOnPage++; + if (null === $this->firstRow) { + if ($firstPage) { + $this->firstRow = true; + } else { + $this->firstRow = false; + } + } elseif (true === $this->firstRow) { + $this->firstRow = false; + } + if ($lastRow === $this->rowNumber) { + $this->lastRow = true; + } + // Hint: do not fetch the body first, the row might want to replace it + $tr = $this->renderRow($row); + $this->add($tr); + } + } + + protected function renderRow($row) + { + return $this::row([$row]); + } + + /** + * @deprecated + * @return bool + */ + protected function isUsEnglish() + { + if ($this->isUsEnglish === null) { + $this->isUsEnglish = in_array(setlocale(LC_ALL, 0), ['en_US', 'en_US.UTF-8', 'C']); + } + + return $this->isUsEnglish; + } + + /** + * @param int $timestamp + */ + protected function renderDayIfNew($timestamp) + { + $day = $this->getDateFormatter()->getFullDay($timestamp); + + if ($this->lastDay !== $day) { + $this->nextHeader()->add( + $this::th($day, [ + 'colspan' => 2, + 'class' => 'table-header-day' + ]) + ); + + $this->lastDay = $day; + $this->nextBody(); + } + } + + abstract protected function fetchQueryRows(); + + public function fetch() + { + $parts = explode('\\', get_class($this)); + $name = end($parts); + Benchmark::measure("Fetching data for $name table"); + $rows = $this->fetchQueryRows(); + $this->fetchedRows = count($rows); + Benchmark::measure("Fetched $this->fetchedRows rows for $name table"); + + return $rows; + } + + protected function initializeOptionalQuickSearch(ControlsAndContent $controller) + { + $columns = $this->getSearchColumns(); + if (! empty($columns)) { + $this->search( + $this->getQuickSearch( + $controller->controls(), + $controller->url() + ) + ); + } + } + + /** + * @param ControlsAndContent $controller + * @return $this + */ + public function renderTo(ControlsAndContent $controller) + { + $url = $controller->url(); + $c = $controller->content(); + $this->paginator = $this->getPaginator($url); + $this->initializeOptionalQuickSearch($controller); + $controller->actions()->add($this->paginator); + $c->add($this); + + // TODO: move elsewhere + if (method_exists($this, 'dumpSqlQuery')) { + if ($url->getParam('format') === 'sql') { + $c->prepend($this->dumpSqlQuery($url)); + } + } + + return $this; + } + + protected function getDateFormatter() + { + if ($this->dateFormatter === null) { + $this->dateFormatter = new LocalDateFormat(); + } + + return $this->dateFormatter; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/SimpleQueryBasedTable.php b/vendor/gipfl/icingaweb2/src/Table/SimpleQueryBasedTable.php new file mode 100644 index 0000000..8d6015a --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/SimpleQueryBasedTable.php @@ -0,0 +1,34 @@ +<?php + +namespace gipfl\IcingaWeb2\Table; + +use Icinga\Data\SimpleQuery; +use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter; + +abstract class SimpleQueryBasedTable extends QueryBasedTable +{ + /** @var SimpleQuery */ + private $query; + + protected function getPaginationAdapter() + { + return new SimpleQueryPaginationAdapter($this->getQuery()); + } + + protected function fetchQueryRows() + { + return $this->query->fetchAll(); + } + + /** + * @return SimpleQuery + */ + public function getQuery() + { + if ($this->query === null) { + $this->query = $this->prepareQuery(); + } + + return $this->query; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Table/ZfQueryBasedTable.php b/vendor/gipfl/icingaweb2/src/Table/ZfQueryBasedTable.php new file mode 100644 index 0000000..8a421d5 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Table/ZfQueryBasedTable.php @@ -0,0 +1,149 @@ +<?php + +namespace gipfl\IcingaWeb2\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\ControlsAndContent; +use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer; +use gipfl\IcingaWeb2\Zf1\Db\SelectPaginationAdapter; +use gipfl\ZfDb\Adapter\Adapter as Db; +use gipfl\ZfDb\Select as DbSelect; +use Icinga\Data\Db\DbConnection; +use Icinga\Data\Filter\Filter; +use ipl\Html\DeferredText; +use ipl\Html\Html; +use LogicException; +use Zend_Db_Adapter_Abstract as DbAdapter; + +abstract class ZfQueryBasedTable extends QueryBasedTable +{ + /** @var ?DbConnection */ + private $connection; + + /** @var DbAdapter|Db */ + private $db; + + private $query; + + private $paginationAdapter; + + public function __construct($db) + { + if ($db instanceof Db || $db instanceof DbAdapter) { + $this->db = $db; + } elseif ($db instanceof DbConnection) { + $this->connection = $db; + $this->db = $db->getDbAdapter(); + } else { + throw new LogicException(sprintf( + 'Unable to deal with %s db class', + get_class($db) + )); + } + } + + public static function show(ControlsAndContent $controller, DbConnection $db) + { + $table = new static($db); + $table->renderTo($controller); + } + + public function getCountQuery() + { + return $this->getPaginationAdapter()->getCountQuery(); + } + + protected function getPaginationAdapter() + { + if ($this->paginationAdapter === null) { + $this->paginationAdapter = new SelectPaginationAdapter($this->getQuery()); + } + + return $this->paginationAdapter; + } + + public function applyFilter(Filter $filter) + { + FilterRenderer::applyToQuery($filter, $this->getQuery()); + return $this; + } + + public function search($search) + { + if (! empty($search)) { + $query = $this->getQuery(); + $columns = $this->getSearchColumns(); + if (strpos($search, ' ') === false) { + $filter = Filter::matchAny(); + foreach ($columns as $column) { + $filter->addFilter(Filter::expression($column, '=', "*$search*")); + } + } else { + $filter = Filter::matchAll(); + foreach (explode(' ', $search) as $s) { + $sub = Filter::matchAny(); + foreach ($columns as $column) { + $sub->addFilter(Filter::expression($column, '=', "*$s*")); + } + $filter->addFilter($sub); + } + } + + FilterRenderer::applyToQuery($filter, $query); + } + + return $this; + } + + protected function fetchQueryRows() + { + return $this->db->fetchAll($this->getQuery()); + } + + /** + * @deprecated Might be null, we'll fade it out + * @return ?DbConnection + */ + public function connection() + { + return $this->connection; + } + + public function db() + { + return $this->db; + } + + /** + * @return DbSelect|\Zend_Db_Select + */ + public function getQuery() + { + if ($this->query === null) { + $this->query = $this->prepareQuery(); + } + + return $this->query; + } + + public function dumpSqlQuery(Url $url) + { + $self = $this; + return Html::tag('div', ['class' => 'sql-dump'], [ + Link::create('[ close ]', $url->without('format')), + Html::tag('h3', null, $this->translate('SQL Query')), + Html::tag('pre', null, new DeferredText( + function () use ($self) { + return wordwrap($self->getQuery()); + } + )), + Html::tag('h3', null, $this->translate('Count Query')), + Html::tag('pre', null, new DeferredText( + function () use ($self) { + return wordwrap($self->getCountQuery()); + } + )), + ]); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Translator.php b/vendor/gipfl/icingaweb2/src/Translator.php new file mode 100644 index 0000000..b1cb088 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Translator.php @@ -0,0 +1,26 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use gipfl\Translation\TranslatorInterface; + +class Translator implements TranslatorInterface +{ + /** @var string */ + private $domain; + + public function __construct($domain) + { + $this->domain = $domain; + } + + public function translate($string) + { + $res = dgettext($this->domain, $string); + if ($res === $string && $this->domain !== 'icinga') { + return dgettext('icinga', $string); + } + + return $res; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Url.php b/vendor/gipfl/icingaweb2/src/Url.php new file mode 100644 index 0000000..2c6bf1f --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Url.php @@ -0,0 +1,162 @@ +<?php + +namespace gipfl\IcingaWeb2; + +use Exception; +use Icinga\Application\Icinga; +use Icinga\Exception\ProgrammingError; +use Icinga\Web\Url as WebUrl; +use Icinga\Web\UrlParams; +use InvalidArgumentException; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UriInterface; +use RuntimeException; + +/** + * Class Url + * + * The main purpose of this class is to get unit tests running on CLI + * Little code from Icinga\Web\Url has been duplicated, as neither fromPath() + * nor getRequest() can be extended in a meaningful way at the time of this + * writing + */ +class Url extends WebUrl +{ + /** + * @param string $url + * @param array $params + * @param null $request + * @return Url + */ + public static function fromPath($url, array $params = array(), $request = null) + { + if ($request === null) { + $request = static::getRequest(); + } + + if (! \is_string($url)) { + throw new InvalidArgumentException(sprintf( + 'url "%s" is not a string', + \var_export($url, 1) + )); + } + + $self = new static; + + if ($url === '#') { + return $self->setPath($url); + } + + $parts = \parse_url($url); + + $self->setBasePath($request->getBaseUrl()); + if (isset($parts['path'])) { + $self->setPath($parts['path']); + } + + if (isset($parts['query'])) { + $params = UrlParams::fromQueryString($parts['query'])->mergeValues($params); + } + + if (isset($parts['fragment'])) { + $self->setAnchor($parts['fragment']); + } + + $self->setParams($params); + return $self; + } + + public static function fromUri(UriInterface $uri) + { + $query = $uri->getQuery(); + $path = $uri->getPath(); + if (\strlen($query)) { + $path .= "?$query"; + } + + return static::fromPath($path); + } + + public static function fromServerRequest(ServerRequestInterface $request) + { + return static::fromUri($request->getUri()); + } + + /** + * Create a new Url class representing the current request + * + * If $params are given, those will be added to the request's parameters + * and overwrite any existing parameters + * + * @param UrlParams|array $params Parameters that should additionally be considered for the url + * @param \Icinga\Web\Request $request A request to use instead of the default one + * + * @return Url + */ + public static function fromRequest($params = array(), $request = null) + { + if ($request === null) { + $request = static::getRequest(); + } + + $url = new Url(); + $url->setPath(\ltrim($request->getPathInfo(), '/')); + $request->getQuery(); + + // $urlParams = UrlParams::fromQueryString($request->getQuery()); + if (isset($_SERVER['QUERY_STRING'])) { + $urlParams = UrlParams::fromQueryString($_SERVER['QUERY_STRING']); + } else { + $urlParams = UrlParams::fromQueryString(''); + foreach ($request->getQuery() as $k => $v) { + $urlParams->set($k, $v); + } + } + + foreach ($params as $k => $v) { + $urlParams->set($k, $v); + } + $url->setParams($urlParams); + $url->setBasePath($request->getBaseUrl()); + + return $url; + } + + public function setBasePath($basePath) + { + if (property_exists($this, 'basePath')) { + parent::setBasePath($basePath); + } else { + $this->setBaseUrl($basePath); + } + + return $this; + } + + public function setParams($params) + { + try { + return parent::setParams($params); + } catch (ProgrammingError $e) { + throw new InvalidArgumentException($e->getMessage(), 0, $e); + } + } + + protected static function getRequest() + { + try { + $app = Icinga::app(); + } catch (ProgrammingError $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + if ($app->isCli()) { + try { + return new FakeRequest(); + } catch (Exception $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + } else { + return $app->getRequest(); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/ActionBar.php b/vendor/gipfl/icingaweb2/src/Widget/ActionBar.php new file mode 100644 index 0000000..63e6c77 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/ActionBar.php @@ -0,0 +1,25 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\BaseHtmlElement; + +class ActionBar extends BaseHtmlElement +{ + protected $contentSeparator = ' '; + + /** @var string */ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'gipfl-action-bar']; + + /** + * @param string $target + * @return $this + */ + public function setBaseTarget($target) + { + $this->getAttributes()->set('data-base-target', $target); + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Content.php b/vendor/gipfl/icingaweb2/src/Widget/Content.php new file mode 100644 index 0000000..92ea115 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Content.php @@ -0,0 +1,14 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\BaseHtmlElement; + +class Content extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $contentSeparator = "\n"; + + protected $defaultAttributes = ['class' => 'content']; +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Controls.php b/vendor/gipfl/icingaweb2/src/Widget/Controls.php new file mode 100644 index 0000000..cb52013 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Controls.php @@ -0,0 +1,163 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; + +class Controls extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $contentSeparator = "\n"; + + protected $defaultAttributes = ['class' => 'controls']; + + /** @var Tabs */ + private $tabs; + + /** @var ActionBar */ + private $actions; + + /** @var string */ + private $title; + + /** @var string */ + private $subTitle; + + /** @var BaseHtmlElement */ + private $titleElement; + + /** + * @param $title + * @param null $subTitle + * @return $this + */ + public function addTitle($title, $subTitle = null) + { + $this->title = $title; + if ($subTitle !== null) { + $this->subTitle = $subTitle; + } + + return $this->setTitleElement($this->renderTitleElement()); + } + + /** + * @param BaseHtmlElement $element + * @return $this + */ + public function setTitleElement(BaseHtmlElement $element) + { + if ($this->titleElement !== null) { + $this->remove($this->titleElement); + } + + $this->titleElement = $element; + $this->prepend($element); + + return $this; + } + + public function getTitleElement() + { + return $this->titleElement; + } + + /** + * @return Tabs + */ + public function getTabs() + { + if ($this->tabs === null) { + $this->tabs = new Tabs(); + } + + return $this->tabs; + } + + /** + * @param Tabs $tabs + * @return $this + */ + public function setTabs(Tabs $tabs) + { + $this->tabs = $tabs; + return $this; + } + + /** + * @param Tabs $tabs + * @return $this + */ + public function prependTabs(Tabs $tabs) + { + if ($this->tabs === null) { + $this->tabs = $tabs; + } else { + $current = $this->tabs->getTabs(); + $this->tabs = $tabs; + foreach ($current as $name => $tab) { + $this->tabs->add($name, $tab); + } + } + + return $this; + } + + /** + * @return ActionBar + */ + public function getActionBar() + { + if ($this->actions === null) { + $this->setActionBar(new ActionBar()); + } + + return $this->actions; + } + + /** + * @param HtmlDocument $actionBar + * @return $this + */ + public function setActionBar(HtmlDocument $actionBar) + { + if ($this->actions !== null) { + $this->remove($this->actions); + } + + $this->actions = $actionBar; + $this->add($actionBar); + + return $this; + } + + /** + * @return BaseHtmlElement + */ + protected function renderTitleElement() + { + $h1 = Html::tag('h1', null, $this->title); + if ($this->subTitle) { + $h1->setSeparator(' ')->add( + Html::tag('small', null, $this->subTitle) + ); + } + + return $h1; + } + + /** + * @return string + */ + public function renderContent() + { + if (null !== $this->tabs) { + $this->prepend($this->tabs); + } + + return parent::renderContent(); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/ControlsAndContent.php b/vendor/gipfl/icingaweb2/src/Widget/ControlsAndContent.php new file mode 100644 index 0000000..8574ce7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/ControlsAndContent.php @@ -0,0 +1,60 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\HtmlDocument; +use gipfl\IcingaWeb2\Url; + +interface ControlsAndContent +{ + /** + * @return Controls + */ + public function controls(); + + /** + * @return Tabs + */ + public function tabs(); + + /** + * @param HtmlDocument|null $actionBar + * @return HtmlDocument + */ + public function actions(HtmlDocument $actionBar = null); + + /** + * @return Content + */ + public function content(); + + /** + * @param $title + * @return $this + */ + public function setTitle($title); + + /** + * @param $title + * @return $this + */ + public function addTitle($title); + + /** + * @param $title + * @param null $url + * @param string $name + * @return $this + */ + public function addSingleTab($title, $url = null, $name = 'main'); + + /** + * @return Url + */ + public function url(); + + /** + * @return Url + */ + public function getOriginalUrl(); +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/ListItem.php b/vendor/gipfl/icingaweb2/src/Widget/ListItem.php new file mode 100644 index 0000000..fa4b562 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/ListItem.php @@ -0,0 +1,26 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\ValidHtml; + +class ListItem extends BaseHtmlElement +{ + protected $contentSeparator = "\n"; + + /** + * @param ValidHtml|array|string $content + * @param Attributes|array $attributes + * + * @return $this + */ + public function addItem($content, $attributes = null) + { + return $this->add( + Html::tag('li', $attributes, $content) + ); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/NameValueTable.php b/vendor/gipfl/icingaweb2/src/Widget/NameValueTable.php new file mode 100644 index 0000000..971a833 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/NameValueTable.php @@ -0,0 +1,29 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use ipl\Html\Table; + +class NameValueTable extends Table +{ + protected $defaultAttributes = ['class' => 'name-value-table']; + + public function createNameValueRow($name, $value) + { + return $this::tr([$this::th($name), $this::td($value)]); + } + + public function addNameValueRow($name, $value) + { + return $this->add($this->createNameValueRow($name, $value)); + } + + public function addNameValuePairs($pairs) + { + foreach ($pairs as $name => $value) { + $this->addNameValueRow($name, $value); + } + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Paginator.php b/vendor/gipfl/icingaweb2/src/Widget/Paginator.php new file mode 100644 index 0000000..3c255a7 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Paginator.php @@ -0,0 +1,463 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use Icinga\Exception\ProgrammingError; +use gipfl\IcingaWeb2\Data\Paginatable; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\Translation\TranslationHelper; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class Paginator extends BaseHtmlElement +{ + use TranslationHelper; + + protected $tag = 'div'; + + protected $defaultAttributes = [ + 'class' => 'pagination-control', + 'role' => 'navigation', + ]; + + /** @var Paginatable The query the paginator widget is created for */ + protected $query; + + /** @var int */ + protected $pageCount; + + /** @var int */ + protected $currentCount; + + /** @var Url */ + protected $url; + + /** @var string */ + protected $pageParam; + + /** @var string */ + protected $perPageParam; + + /** @var int */ + protected $totalCount; + + /** @var int */ + protected $defaultItemCountPerPage = 25; + + public function __construct( + Paginatable $query, + Url $url, + $pageParameter = 'page', + $perPageParameter = 'limit' + ) { + $this->query = $query; + $this->setPageParam($pageParameter); + $this->setPerPageParam($perPageParameter); + $this->setUrl($url); + } + + public function setItemsPerPage($count) + { + // TODO: this should become setOffset once available + $query = $this->getQuery(); + $query->setLimit($count); + + return $this; + } + + protected function setPageParam($pageParam) + { + $this->pageParam = $pageParam; + return $this; + } + + protected function setPerPageParam($perPageParam) + { + $this->perPageParam = $perPageParam; + return $this; + } + + public function getPageParam() + { + return $this->pageParam; + } + + public function getPerPageParam() + { + return $this->perPageParam; + } + + public function getCurrentPage() + { + $query = $this->getQuery(); + if ($query->hasOffset()) { + return ($query->getOffset() / $this->getItemsPerPage()) + 1; + } else { + return 1; + } + } + + protected function setCurrentPage($page) + { + $page = (int) $page; + $offset = $this->firstRowOnPage($page) - 1; + if ($page > 1) { + $query = $this->getQuery(); + $query->setOffset($offset); + } + } + + public function getPageCount() + { + if ($this->pageCount === null) { + $this->pageCount = (int) ceil($this->getTotalItemCount() / $this->getItemsPerPage()); + } + + return $this->pageCount; + } + + protected function getItemsPerPage() + { + $limit = $this->getQuery()->getLimit(); + if ($limit === null) { + throw new ProgrammingError('Something went wrong, got no limit when there should be one'); + } else { + return $limit; + } + } + + public function getTotalItemCount() + { + if ($this->totalCount === null) { + $this->totalCount = count($this->getQuery()); + } + + return $this->totalCount; + } + + public function getPrevious() + { + if ($this->hasPrevious()) { + return $this->getCurrentPage() - 1; + } else { + return null; + } + } + + public function hasPrevious() + { + return $this->getCurrentPage() > 1; + } + + public function getNext() + { + if ($this->hasNext()) { + return $this->getCurrentPage() + 1; + } else { + return null; + } + } + + public function hasNext() + { + return $this->getCurrentPage() < $this->getPageCount(); + } + + public function getQuery() + { + return $this->query; + } + + /** + * Returns an array of "local" pages given the page count and current page number + * + * @return array + */ + protected function getPages() + { + $page = $this->getPageCount(); + $current = $this->getCurrentPage(); + + $range = []; + + if ($page < 10) { + // Show all pages if we have less than 10 + for ($i = 1; $i < 10; $i++) { + if ($i > $page) { + break; + } + + $range[$i] = $i; + } + } else { + // More than 10 pages: + foreach ([1, 2] as $i) { + $range[$i] = $i; + } + + if ($current < 6) { + // We are on page 1-5 from + for ($i = 1; $i <= 7; $i++) { + $range[$i] = $i; + } + } else { + // Current page > 5 + $range[] = '…'; + + if (($page - $current) < 5) { + // Less than 5 pages left + $start = 5 - ($page - $current); + } else { + $start = 1; + } + + for ($i = $current - $start; $i < ($current + (4 - $start)); $i++) { + if ($i > $page) { + break; + } + + $range[$i] = $i; + } + } + + if ($current < ($page - 2)) { + $range[] = '…'; + } + + foreach ([$page - 1, $page] as $i) { + $range[$i] = $i; + } + } + + if (empty($range)) { + $range[] = 1; + } + + return $range; + } + + public function getDefaultItemCountPerPage() + { + return $this->defaultItemCountPerPage; + } + + public function setDefaultItemCountPerPage($count) + { + $this->defaultItemCountPerPage = (int) $count; + return $this; + } + + public function setUrl(Url $url) + { + $page = (int) $url->shift($this->getPageParam()); + $perPage = (int) $url->getParam($this->getPerPageParam()); + if ($perPage > 0) { + $this->setItemsPerPage($perPage); + } else { + if (! $this->getQuery()->hasLimit()) { + $this->setItemsPerPage($this->getDefaultItemCountPerPage()); + } + } + if ($page > 0) { + $this->setCurrentPage($page); + } + + $this->url = $url; + + return $this; + } + + public function getUrl() + { + if ($this->url === null) { + $this->setUrl(Url::fromRequest()); + } + + return $this->url; + } + + public function getPreviousLabel() + { + return $this->getLabel($this->getCurrentPage() - 1); + } + + protected function getNextLabel() + { + return $this->getLabel($this->getCurrentPage() + 1); + } + + protected function getLabel($page) + { + return sprintf( + $this->translate('Show rows %u to %u of %u'), + $this->firstRowOnPage($page), + $this->lastRowOnPage($page), + $this->getTotalItemCount() + ); + } + + protected function renderPrevious() + { + return Html::tag('li', [ + 'class' => 'nav-item' + ], Link::create( + Icon::create('angle-double-left'), + $this->makeUrl($this->getPrevious()), + null, + [ + 'title' => $this->getPreviousLabel(), + 'class' => 'previous-page' + ] + )); + } + + protected function renderNoPrevious() + { + return $this->renderDisabled(Html::tag('span', [ + 'class' => 'previous-page' + ], [ + $this->srOnly($this->translate('Previous page')), + Icon::create('angle-double-left') + ])); + } + + protected function renderNext() + { + return Html::tag('li', [ + 'class' => 'nav-item' + ], Link::create( + Icon::create('angle-double-right'), + $this->makeUrl($this->getNext()), + null, + [ + 'title' => $this->getNextLabel(), + 'class' => 'next-page' + ] + )); + } + + protected function renderNoNext() + { + return $this->renderDisabled(Html::tag('span', [ + 'class' => 'previous-page' + ], [ + $this->srOnly($this->translate('Next page')), + Icon::create('angle-double-right') + ])); + } + + protected function renderDots() + { + return $this->renderDisabled(Html::tag('span', null, '…')); + } + + protected function renderInnerPages() + { + $pages = []; + $current = $this->getCurrentPage(); + + foreach ($this->getPages() as $page) { + if ($page === '…') { + $pages[] = $this->renderDots(); + } else { + $pages[] = Html::tag( + 'li', + $page === $current ? ['class' => 'active'] : null, + $this->makeLink($page) + ); + } + } + + return $pages; + } + + protected function lastRowOnPage($page) + { + $perPage = $this->getItemsPerPage(); + $total = $this->getTotalItemCount(); + $last = $page * $perPage; + if ($last > $total) { + $last = $total; + } + + return $last; + } + + protected function firstRowOnPage($page) + { + return ($page - 1) * $this->getItemsPerPage() + 1; + } + + protected function makeLink($page) + { + return Link::create( + $page, + $this->makeUrl($page), + null, + ['title' => $this->getLabel($page)] + ); + } + + protected function makeUrl($page) + { + if ($page) { + return $this->getUrl()->with('page', $page); + } else { + return $this->getUrl(); + } + } + + protected function srOnly($content) + { + return Html::tag('span', ['class' => 'sr-only'], $content); + } + + protected function renderDisabled($content) + { + return Html::tag('li', [ + 'class' => ['nav-item', 'disabled'], + 'aria-hidden' => 'true' + ], $content); + } + + protected function renderList() + { + return Html::tag( + 'ul', + ['class' => ['nav', 'tab-nav']], + [ + $this->hasPrevious() ? $this->renderPrevious() : $this->renderNoPrevious(), + $this->renderInnerPages(), + $this->hasNext() ? $this->renderNext() : $this->renderNoNext() + ] + ); + } + + public function assemble() + { + $this->add([ + $this->renderScreenReaderHeader(), + $this->renderList() + ]); + } + + protected function renderScreenReaderHeader() + { + return Html::tag('h2', [ + // 'id' => $this->protectId('pagination') -> why? + 'class' => 'sr-only', + 'tab-index' => '-1' + ], $this->translate('Pagination')); + } + + public function render() + { + if ($this->getPageCount() < 2) { + return ''; + } else { + return parent::render(); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Widget/Tabs.php b/vendor/gipfl/icingaweb2/src/Widget/Tabs.php new file mode 100644 index 0000000..38bf4cd --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Widget/Tabs.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\IcingaWeb2\Widget; + +use Exception; +use Icinga\Web\Widget\Tabs as WebTabs; +use InvalidArgumentException; +use ipl\Html\ValidHtml; + +class Tabs extends WebTabs implements ValidHtml +{ + /** + * @param string $name + * @return $this + */ + public function activate($name) + { + try { + parent::activate($name); + } catch (Exception $e) { + throw new InvalidArgumentException( + "Can't activate '$name', there is no such tab" + ); + } + + return $this; + } + + /** + * @param string $name + * @param array|\Icinga\Web\Widget\Tab $tab + * @return $this + */ + public function add($name, $tab) + { + try { + parent::add($name, $tab); + } catch (Exception $e) { + throw new InvalidArgumentException($e->getMessage()); + } + + return $this; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/Db/CountQuery.php b/vendor/gipfl/icingaweb2/src/Zf1/Db/CountQuery.php new file mode 100644 index 0000000..07204b8 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/Db/CountQuery.php @@ -0,0 +1,93 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1\Db; + +use gipfl\ZfDb\Select; +use RuntimeException; +use Zend_Db_Select as ZfSelect; + +class CountQuery +{ + /** @var Select|ZfSelect */ + private $query; + + private $maxRows; + + /** + * ZfCountQuery constructor. + * @param Select|ZfSelect $query + */ + public function __construct($query) + { + if ($query instanceof Select || $query instanceof ZfSelect) { + $this->query = $query; + } else { + throw new RuntimeException('Got no supported ZF1 Select object'); + } + } + + public function setMaxRows($max) + { + $this->maxRows = $max; + return $this; + } + + public function getQuery() + { + if ($this->needsSubQuery()) { + return $this->buildSubQuery(); + } else { + return $this->buildSimpleQuery(); + } + } + + protected function hasOneOf($parts) + { + foreach ($parts as $part) { + if ($this->hasPart($part)) { + return true; + } + } + + return false; + } + + protected function hasPart($part) + { + $values = $this->query->getPart($part); + return ! empty($values); + } + + protected function needsSubQuery() + { + return null !== $this->maxRows || $this->hasOneOf([ + Select::GROUP, + Select::UNION + ]); + } + + protected function buildSubQuery() + { + $sub = clone($this->query); + $sub->limit(null, null); + $class = $this->query; + $query = new $class($this->query->getAdapter()); + $query->from($sub, ['cnt' => 'COUNT(*)']); + if (null !== $this->maxRows) { + $sub->limit($this->maxRows + 1); + } + + return $query; + } + + protected function buildSimpleQuery() + { + $query = clone($this->query); + $query->reset(Select::COLUMNS); + $query->reset(Select::ORDER); + $query->reset(Select::LIMIT_COUNT); + $query->reset(Select::LIMIT_OFFSET); + $query->columns(['cnt' => 'COUNT(*)']); + return $query; + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/Db/FilterRenderer.php b/vendor/gipfl/icingaweb2/src/Zf1/Db/FilterRenderer.php new file mode 100644 index 0000000..b51296f --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/Db/FilterRenderer.php @@ -0,0 +1,335 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1\Db; + +use gipfl\ZfDb\Adapter\Adapter as Db; +use gipfl\ZfDb\Exception\SelectException; +use gipfl\ZfDb\Expr; +use gipfl\ZfDb\Select; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterAnd; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Data\Filter\FilterNot; +use Icinga\Data\Filter\FilterOr; +use Icinga\Data\SimpleQuery; +use InvalidArgumentException; +use RuntimeException; +use Zend_Db_Adapter_Abstract as DbAdapter; +use Zend_Db_Expr as DbExpr; +use Zend_Db_Select as DbSelect; +use Zend_Db_Select_Exception as DbSelectException; + +class FilterRenderer +{ + private $db; + + /** @var Filter */ + private $filter; + + /** @var array */ + private $columnMap; + + /** @var string */ + private $dbExprClass; + + /** + * FilterRenderer constructor. + * @param Filter $filter + * @param Db|DbAdapter $db + */ + public function __construct(Filter $filter, $db) + { + $this->filter = $filter; + if ($db instanceof Db) { + $this->db = $db; + $this->dbExprClass = Expr::class; + } elseif ($db instanceof DbAdapter) { + $this->db = $db; + $this->dbExprClass = DbExpr::class; + } else { + throw new RuntimeException('Got no supported ZF1 DB adapter'); + } + } + + /** + * @return Expr|DbExpr + */ + public function toDbExpression() + { + return $this->expr($this->render()); + } + + /** + * @return Expr|DbExpr + */ + protected function expr($content) + { + $class = $this->dbExprClass; + return new $class($content); + } + + /** + * @param Filter $filter + * @param Select|DbSelect|SimpleQuery $query + * @return Select|DbSelect|SimpleQuery + */ + public static function applyToQuery(Filter $filter, $query) + { + if ($query instanceof SimpleQuery) { + $query->applyFilter($filter); + return $query; + } + if (! ($query instanceof Select || $query instanceof DbSelect)) { + throw new RuntimeException('Got no supported ZF1 Select object'); + } + + if (! $filter->isEmpty()) { + $renderer = new static($filter, $query->getAdapter()); + $renderer->extractColumnMap($query); + $query->where($renderer->toDbExpression()); + } + + return $query; + } + + protected function lookupColumnAlias($column) + { + if (array_key_exists($column, $this->columnMap)) { + return $this->columnMap[$column]; + } else { + return $column; + } + } + + protected function extractColumnMap($query) + { + $map = []; + try { + $columns = $query->getPart(Select::COLUMNS); + } catch (SelectException $e) { + // Will not happen. + throw new RuntimeException($e->getMessage()); + } catch (DbSelectException $e) { + // Will not happen. + throw new RuntimeException($e->getMessage()); + } + + foreach ($columns as $col) { + if ($col[1] instanceof Expr || $col[1] instanceof DbExpr) { + $map[$col[2]] = (string) $col[1]; + $map[$col[2]] = $col[1]; + } else { + $map[$col[2]] = $col[0] . '.' . $col[1]; + } + } + + $this->columnMap = $map; + } + + /** + * @return string + */ + public function render() + { + return $this->renderFilter($this->filter); + } + + protected function renderFilterChain(FilterChain $filter, $level = 0) + { + $prefix = ''; + + if ($filter instanceof FilterAnd) { + $op = ' AND '; + } elseif ($filter instanceof FilterOr) { + $op = ' OR '; + } elseif ($filter instanceof FilterNot) { + $op = ' AND '; + $prefix = 'NOT '; + } else { + throw new InvalidArgumentException( + 'Cannot render a %s filter chain for Zf Db', + get_class($filter) + ); + } + + $parts = []; + if ($filter->isEmpty()) { + // Hint: we might want to fail here + return ''; + } else { + foreach ($filter->filters() as $f) { + $part = $this->renderFilter($f, $level + 1); + if ($part !== '') { + $parts[] = $part; + } + } + if (empty($parts)) { + // will not happen, as we are not empty + return ''; + } else { + if ($level > 0) { + return "$prefix (" . implode($op, $parts) . ')'; + } else { + return $prefix . implode($op, $parts); + } + } + } + } + + protected function renderFilterExpression(FilterExpression $filter) + { + $col = $this->lookupColumnAlias($filter->getColumn()); + if (! $col instanceof Expr && ! $col instanceof DbExpr && ! ctype_digit($col)) { + $col = $this->db->quoteIdentifier($col); + } + $sign = $filter->getSign(); + $expression = $filter->getExpression(); + + if (is_array($expression)) { + return $this->renderArrayExpression($col, $sign, $expression); + } + + if ($sign === '=') { + if (strpos($expression, '*') === false) { + return $this->renderAny($col, $sign, $expression); + } else { + return $this->renderLike($col, $expression); + } + } + + if ($sign === '!=') { + if (strpos($expression, '*') === false) { + return $this->renderAny($col, $sign, $expression); + } else { + return $this->renderNotLike($col, $expression); + } + } + + return $this->renderAny($col, $sign, $expression); + } + + + protected function renderLike($col, $expression) + { + if ($expression === '*') { + return $this->expr('TRUE'); + } + + return $col . ' LIKE ' . $this->escape($this->escapeWildcards($expression)); + } + + protected function renderNotLike($col, $expression) + { + if ($expression === '*') { + return $this->expr('FALSE'); + } + + return sprintf( + '(%1$s NOT LIKE %2$s OR %1$s IS NULL)', + $col, + $this->escape($this->escapeWildcards($expression)) + ); + } + + protected function renderNotEqual($col, $expression) + { + return sprintf('(%1$s != %2$s OR %1$s IS NULL)', $col, $this->escape($expression)); + } + + protected function renderAny($col, $sign, $expression) + { + return sprintf('%s %s %s', $col, $sign, $this->escape($expression)); + } + + protected function renderArrayExpression($col, $sign, $expression) + { + if ($sign === '=') { + return $col . ' IN (' . $this->escape($expression) . ')'; + } elseif ($sign === '!=') { + return sprintf( + '(%1$s NOT IN (%2$s) OR %1$s IS NULL)', + $col, + $this->escape($expression) + ); + } + + throw new InvalidArgumentException( + 'Array expressions can only be rendered for = and !=, got %s', + $sign + ); + } + + /** + * @param Filter $filter + * @param int $level + * @return string|Expr|DbExpr + */ + protected function renderFilter(Filter $filter, $level = 0) + { + if ($filter instanceof FilterChain) { + return $this->renderFilterChain($filter, $level); + } elseif ($filter instanceof FilterExpression) { + return $this->renderFilterExpression($filter); + } else { + throw new RuntimeException(sprintf( + 'Filter of type FilterChain or FilterExpression expected, got %s', + get_class($filter) + )); + } + } + + protected function escape($value) + { + // bindParam? bindValue? + if (is_array($value)) { + $ret = []; + foreach ($value as $val) { + $ret[] = $this->escape($val); + } + return implode(', ', $ret); + } else { + return $this->db->quote($value); + } + } + + protected function escapeWildcards($value) + { + return preg_replace('/\*/', '%', $value); + } + + public function whereToSql($col, $sign, $expression) + { + if (is_array($expression)) { + if ($sign === '=') { + return $col . ' IN (' . $this->escape($expression) . ')'; + } elseif ($sign === '!=') { + return sprintf('(%1$s NOT IN (%2$s) OR %1$s IS NULL)', $col, $this->escape($expression)); + } + + throw new InvalidArgumentException( + 'Unable to render array expressions with operators other than equal or not equal' + ); + } elseif ($sign === '=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + return $this->expr('TRUE'); + } + + return $col . ' LIKE ' . $this->escape($this->escapeWildcards($expression)); + } elseif ($sign === '!=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + return $this->expr('FALSE'); + } + + return sprintf( + '(%1$s NOT LIKE %2$s OR %1$s IS NULL)', + $col, + $this->escape($this->escapeWildcards($expression)) + ); + } elseif ($sign === '!=') { + return sprintf('(%1$s %2$s %3$s OR %1$s IS NULL)', $col, $sign, $this->escape($expression)); + } else { + return sprintf('%s %s %s', $col, $sign, $this->escape($expression)); + } + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/Db/SelectPaginationAdapter.php b/vendor/gipfl/icingaweb2/src/Zf1/Db/SelectPaginationAdapter.php new file mode 100644 index 0000000..599a3ee --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/Db/SelectPaginationAdapter.php @@ -0,0 +1,111 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1\Db; + +use gipfl\IcingaWeb2\Data\Paginatable; +use gipfl\ZfDb\Select; +use gipfl\ZfDb\Exception\SelectException; +use Icinga\Application\Benchmark; +use RuntimeException; +use Zend_Db_Select as ZfSelect; +use Zend_Db_Select_Exception as ZfDbSelectException; + +class SelectPaginationAdapter implements Paginatable +{ + private $query; + + private $countQuery; + + private $cachedCount; + + private $cachedCountQuery; + + public function __construct($query) + { + if ($query instanceof Select || $query instanceof ZfSelect) { + $this->query = $query; + } else { + throw new RuntimeException('Got no supported ZF1 Select object'); + } + } + + public function getCountQuery() + { + if ($this->countQuery === null) { + $this->countQuery = (new CountQuery($this->query))->getQuery(); + } + + return $this->countQuery; + } + + #[\ReturnTypeWillChange] + public function count() + { + $queryString = (string) $this->getCountQuery(); + if ($this->cachedCountQuery !== $queryString) { + Benchmark::measure('Running count() for pagination'); + $this->cachedCountQuery = $queryString; + $count = $this->query->getAdapter()->fetchOne( + $queryString + ); + $this->cachedCount = $count; + Benchmark::measure("Counted $count rows"); + } + + return $this->cachedCount; + } + + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + } + + public function hasLimit() + { + return $this->getLimit() !== null; + } + + public function getLimit() + { + return $this->getQueryPart(Select::LIMIT_COUNT); + } + + public function setLimit($limit) + { + $this->query->limit( + $limit, + $this->getOffset() + ); + } + + public function hasOffset() + { + return $this->getOffset() !== null; + } + + public function getOffset() + { + return $this->getQueryPart(Select::LIMIT_OFFSET); + } + + protected function getQueryPart($part) + { + try { + return $this->query->getPart($part); + } catch (SelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } catch (ZfDbSelectException $e) { + // Will not happen if $part is correct. + throw new RuntimeException($e); + } + } + + public function setOffset($offset) + { + $this->query->limit( + $this->getLimit(), + $offset + ); + } +} diff --git a/vendor/gipfl/icingaweb2/src/Zf1/SimpleViewRenderer.php b/vendor/gipfl/icingaweb2/src/Zf1/SimpleViewRenderer.php new file mode 100644 index 0000000..89b36a4 --- /dev/null +++ b/vendor/gipfl/icingaweb2/src/Zf1/SimpleViewRenderer.php @@ -0,0 +1,115 @@ +<?php + +namespace gipfl\IcingaWeb2\Zf1; + +use gipfl\IcingaWeb2\Widget\Content; +use gipfl\IcingaWeb2\Widget\Controls; +use ipl\Html\Error; +use Icinga\Application\Icinga; +use ipl\Html\ValidHtml; +use Zend_Controller_Action_Helper_Abstract as Helper; +use Zend_Controller_Action_HelperBroker as HelperBroker; + +class SimpleViewRenderer extends Helper implements ValidHtml +{ + private $disabled = false; + + private $rendered = false; + + /** @var \Zend_View_Interface */ + public $view; + + public function init() + { + // Register view with action controller (unless already registered) + if ((null !== $this->_actionController) && (null === $this->_actionController->view)) { + $this->_actionController->view = $this->view; + } + } + + public function disable($disabled = true) + { + $this->disabled = $disabled; + return $this; + } + + public function replaceZendViewRenderer() + { + /** @var \Zend_Controller_Action_Helper_ViewRenderer $viewRenderer */ + $viewRenderer = Icinga::app()->getViewRenderer(); + $viewRenderer->setNeverRender(); + $viewRenderer->setNeverController(); + HelperBroker::removeHelper('viewRenderer'); + HelperBroker::addHelper($this); + $this->view = $viewRenderer->view; + return $this; + } + + public function render($action = null, $name = null, $noController = null) + { + if (null === $name) { + $name = null; // $this->getResponseSegment(); + } + // Compat. + if (isset($this->_actionController) + && get_class($this->_actionController) === 'Icinga\\Controllers\\ErrorController' + ) { + $html = $this->simulateErrorController(); + } else { + $html = ''; + if (null !== $this->view->controls) { + $html .= $this->view->controls->__toString(); + } + + if (null !== $this->view->content) { + $html .= $this->view->content->__toString(); + } + } + + $this->getResponse()->appendBody($html, $name); + // $this->setNoRender(); + $this->rendered = true; + } + + protected function simulateErrorController() + { + $errorHandler = $this->_actionController->getParam('error_handler'); + if (isset($errorHandler->exception)) { + $error = Error::show($errorHandler->exception); + } else { + $error = 'An unknown error occured'; + } + + /** @var \Icinga\Web\Request $request */ + $request = $this->getRequest(); + $controls = new Controls(); + $controls->getTabs()->add('error', [ + 'label' => t('Error'), + 'url' => $request->getUrl(), + ])->activate('error'); + $content = new Content(); + $content->add($error); + + return $controls . $content; + } + + public function shouldRender() + { + return ! $this->disabled && ! $this->rendered; + } + + public function postDispatch() + { + if ($this->shouldRender()) { + $this->render(); + } + } + + public function getName() + { + // TODO: This is wrong, should be 'viewRenderer' - but that would + // currently break nearly everything, starting with full layout + // rendering + return 'ViewRenderer'; + } +} diff --git a/vendor/gipfl/influxdb/LICENSE b/vendor/gipfl/influxdb/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/influxdb/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/influxdb/composer.json b/vendor/gipfl/influxdb/composer.json new file mode 100644 index 0000000..8c9aec4 --- /dev/null +++ b/vendor/gipfl/influxdb/composer.json @@ -0,0 +1,25 @@ +{ + "name": "gipfl/influxdb", + "description": "InfluxDB client library", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "gipfl\\InfluxDb\\": "src/" + } + }, + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "require": { + "php": ">=5.6.0", + "ext-ctype": "*", + "ext-pcntl": "*", + "gipfl/curl": ">=0.1.1", + "react/event-loop": ">=1.1", + "gipfl/json": ">=0.2" + } +} diff --git a/vendor/gipfl/influxdb/src/ChunkedInfluxDbWriter.php b/vendor/gipfl/influxdb/src/ChunkedInfluxDbWriter.php new file mode 100644 index 0000000..37473a7 --- /dev/null +++ b/vendor/gipfl/influxdb/src/ChunkedInfluxDbWriter.php @@ -0,0 +1,158 @@ +<?php + +namespace gipfl\InfluxDb; + +use gipfl\Curl\RequestError; +use Psr\Http\Message\ResponseInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; + +/** + * Gives no result, enqueue and forget + */ +class ChunkedInfluxDbWriter implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + const DEFAULT_BUFFER_SIZE = 5000; + + const DEFAULT_FLUSH_INTERVAL = 0.2; + + const DEFAULT_PRECISION = 's'; + + /** @var int */ + protected $bufferSize = self::DEFAULT_BUFFER_SIZE; + + /** @var float */ + protected $flushInterval = self::DEFAULT_FLUSH_INTERVAL; + + /** @var string */ + protected $precision = self::DEFAULT_PRECISION; + + /** @var DataPoint[] */ + protected $buffer = []; + + /** @var InfluxDbConnection */ + protected $connection; + + /** @var string */ + protected $dbName; + + /** @var LoopInterface */ + protected $loop; + + /** @var ?TimerInterface */ + protected $flushTimer; + + public function __construct(InfluxDbConnection $connection, $dbName, LoopInterface $loop) + { + $this->setLogger(new NullLogger()); + $this->connection = $connection; + $this->dbName = $dbName; + $this->loop = $loop; + } + + /** + * @param DataPoint $point + */ + public function enqueue(DataPoint $point) + { + $this->buffer[] = $point; + $count = count($this->buffer); + if ($count >= $this->bufferSize) { + $this->flush(); + } else { + $this->startFlushTimer(); + } + } + + /** + * @param int $bufferSize + * @return ChunkedInfluxDbWriter + */ + public function setBufferSize($bufferSize) + { + $this->bufferSize = $bufferSize; + return $this; + } + + /** + * @param float $flushInterval + * @return ChunkedInfluxDbWriter + */ + public function setFlushInterval($flushInterval) + { + $this->flushInterval = $flushInterval; + return $this; + } + + /** + * @param string $precision ns,u,ms,s,m,h + * @return ChunkedInfluxDbWriter + */ + public function setPrecision($precision) + { + $this->precision = $precision; + return $this; + } + + public function flush() + { + $buffer = $this->buffer; + $this->buffer = []; + $this->stopFlushTimer(); + $this->logger->debug(sprintf('Flushing InfluxDB buffer, sending %d data points', count($buffer))); + $start = microtime(true); + $this->connection->writeDataPoints($this->dbName, $buffer, $this->precision) + ->then(function (ResponseInterface $response) use ($start) { + $code = $response->getStatusCode(); + $duration = (microtime(true) - $start) * 1000; + if ($code > 199 && $code < 300) { + $this->logger->debug(sprintf('Got response from InfluxDB after %.2Fms', $duration)); + } else { + $this->logger->error(sprintf( + 'Got unexpected %d from InfluxDB after %.2Fms: %s', + $code, + $duration, + $response->getReasonPhrase() + )); + } + }, function (RequestError $e) { + $this->logger->error($e->getMessage()); + })->done(); + } + + public function stop() + { + $this->flush(); + } + + protected function startFlushTimer() + { + if ($this->flushTimer === null) { + $this->flushTimer = $this->loop->addPeriodicTimer($this->flushInterval, function () { + if (! empty($this->buffer)) { + $this->flush(); + } + }); + } + } + + protected function stopFlushTimer() + { + if ($this->flushTimer) { + $this->loop->cancelTimer($this->flushTimer); + $this->flushTimer = null; + } + } + + public function __destruct() + { + $this->stopFlushTimer(); + $this->loop = null; + $this->connection = null; + } +} diff --git a/vendor/gipfl/influxdb/src/DataPoint.php b/vendor/gipfl/influxdb/src/DataPoint.php new file mode 100644 index 0000000..f272206 --- /dev/null +++ b/vendor/gipfl/influxdb/src/DataPoint.php @@ -0,0 +1,63 @@ +<?php + +namespace gipfl\InfluxDb; + +use InvalidArgumentException; +use function array_key_exists; +use function array_merge; +use function is_array; +use function is_object; +use function ksort; + +class DataPoint +{ + protected $timestamp; + + protected $measurement; + + protected $tags = []; + + protected $fields; + + public function __construct($measurement, $tags = [], $fields = [], $timestamp = null) + { + $this->measurement = (string) $measurement; + if ($timestamp !== null) { + $this->timestamp = $timestamp; + } + + if (! empty($tags)) { + $this->addTags($tags); + } + + if (is_array($fields) || is_object($fields)) { + $this->fields = (array) $fields; + } else { + $this->fields = ['value' => $fields]; + } + + if (empty($this->fields)) { + throw new InvalidArgumentException('At least one field/value is required'); + } + } + + public function addTags($tags) + { + $this->tags = array_merge($this->tags, (array) $tags); + ksort($this->tags); + } + + public function getTag($name, $default = null) + { + if (array_key_exists($name, $this->tags)) { + return $this->tags[$name]; + } else { + return $default; + } + } + + public function __toString() + { + return LineProtocol::renderMeasurement($this->measurement, $this->tags, $this->fields, $this->timestamp); + } +} diff --git a/vendor/gipfl/influxdb/src/Escape.php b/vendor/gipfl/influxdb/src/Escape.php new file mode 100644 index 0000000..e6cb555 --- /dev/null +++ b/vendor/gipfl/influxdb/src/Escape.php @@ -0,0 +1,67 @@ +<?php + +namespace gipfl\InfluxDb; + +use InvalidArgumentException; +use function addcslashes; +use function ctype_digit; +use function is_bool; +use function is_int; +use function is_null; +use function preg_match; +use function strpos; + +abstract class Escape +{ + const ESCAPE_COMMA_SPACE = ' ,\\'; + + const ESCAPE_COMMA_EQUAL_SPACE = ' =,\\'; + + const ESCAPE_DOUBLE_QUOTES = '"\\'; + + const NULL = 'null'; + + const TRUE = 'true'; + + const FALSE = 'false'; + + public static function measurement($value) + { + static::assertNoNewline($value); + return addcslashes($value, self::ESCAPE_COMMA_SPACE); + } + + public static function key($value) + { + static::assertNoNewline($value); + return addcslashes($value, self::ESCAPE_COMMA_EQUAL_SPACE); + } + + public static function tagValue($value) + { + static::assertNoNewline($value); + return addcslashes($value, self::ESCAPE_COMMA_EQUAL_SPACE); + } + + public static function fieldValue($value) + { + // Faster checks first + if (is_int($value) || ctype_digit($value) || preg_match('/^-\d+$/', $value)) { + return "{$value}i"; + } elseif (is_bool($value)) { + return $value ? self::TRUE : self::FALSE; + } elseif (is_null($value)) { + return self::NULL; + } else { + static::assertNoNewline($value); + return '"' . addcslashes($value, self::ESCAPE_DOUBLE_QUOTES) . '"'; + } + } + + protected static function assertNoNewline($value) + { + if (strpos($value, "\n") !== false) { + throw new InvalidArgumentException('Newlines are forbidden in InfluxDB line protocol'); + } + } +} diff --git a/vendor/gipfl/influxdb/src/InfluxDbConnection.php b/vendor/gipfl/influxdb/src/InfluxDbConnection.php new file mode 100644 index 0000000..d20944a --- /dev/null +++ b/vendor/gipfl/influxdb/src/InfluxDbConnection.php @@ -0,0 +1,24 @@ +<?php + +namespace gipfl\InfluxDb; + +interface InfluxDbConnection +{ + public function ping($verbose = false); + + public function getVersion(); + + public function listDatabases(); + + public function createDatabase($name); + + public function getHealth(); + + /** + * @param string $dbName + * @param DataPoint[] $dataPoints + * @param string|null $precision ns,u,ms,s,m,h + * @return \React\Promise\Promise + */ + public function writeDataPoints($dbName, array $dataPoints, $precision = null); +} diff --git a/vendor/gipfl/influxdb/src/InfluxDbConnectionFactory.php b/vendor/gipfl/influxdb/src/InfluxDbConnectionFactory.php new file mode 100644 index 0000000..f260010 --- /dev/null +++ b/vendor/gipfl/influxdb/src/InfluxDbConnectionFactory.php @@ -0,0 +1,38 @@ +<?php + +namespace gipfl\InfluxDb; + +use gipfl\Curl\CurlAsync; +use React\EventLoop\LoopInterface; +use React\Promise\Promise; +use RuntimeException; + +abstract class InfluxDbConnectionFactory +{ + /** + * AsyncInfluxDbWriter constructor. + * @param LoopInterface $loop + * @param $baseUrl string InfluxDB base URL + * @param string|null $username + * @param string|null $password + * @return Promise <InfluxDbConnection> + */ + public static function create(CurlAsync $curl, $baseUrl, $username = null, $password = null) + { + $v1 = new InfluxDbConnectionV1($curl, $baseUrl); + return $v1->getVersion()->then(function ($version) use ($baseUrl, $username, $password, $curl, $v1) { + if ($version === null || preg_match('/^v?2\./', $version)) { + $v2 = new InfluxDbConnectionV2($curl, $baseUrl, $username, $password); + return $v2->getVersion()->then(function ($version) use ($v2) { + if ($version === null) { + throw new RuntimeException('Unable to detect InfluxDb version'); + } else { + return $v2; + } + }); + } else { + return $v1; + } + }); + } +} diff --git a/vendor/gipfl/influxdb/src/InfluxDbConnectionV1.php b/vendor/gipfl/influxdb/src/InfluxDbConnectionV1.php new file mode 100644 index 0000000..0b674c2 --- /dev/null +++ b/vendor/gipfl/influxdb/src/InfluxDbConnectionV1.php @@ -0,0 +1,311 @@ +<?php + +namespace gipfl\InfluxDb; + +use gipfl\Curl\CurlAsync; +use gipfl\Json\JsonString; +use Psr\Http\Message\ResponseInterface; +use Ramsey\Uuid\Uuid; +use React\Promise\Promise; +use function React\Promise\resolve; + +class InfluxDbConnectionV1 implements InfluxDbConnection +{ + const API_VERSION = 'v1'; + + const USER_AGENT = 'gipfl-InfluxDB/0.5'; + + /** @var string */ + protected $baseUrl; + + protected $version; + + /** @var string|null */ + protected $username; + + /** @var string|null */ + protected $password; + + protected $curl; + + /** + * AsyncInfluxDbWriter constructor. + * @param CurlAsync $curl + * @param string $baseUrl InfluxDB base URL + * @param ?string $username + * @param ?string $password + */ + public function __construct(CurlAsync $curl, $baseUrl, $username = null, $password = null) + { + $this->baseUrl = rtrim($baseUrl, '/'); + $this->curl = $curl; + $this->setUsername($username); + $this->setPassword($password); + } + + /** + * @param string|null $username + * @return $this + */ + public function setUsername($username) + { + $this->username = $username; + return $this; + } + + /** + * @param string|null $password + * @return $this + */ + public function setPassword($password) + { + $this->password = $password; + return $this; + } + + public function ping($verbose = false) + { + $params = []; + if ($verbose) { + $params['verbose'] = 'true'; + } + return $this->getUrl('ping', $params); + } + + public function getVersion() + { + if ($this->version) { + return resolve($this->version); + } + + return $this->get('ping')->then(function (ResponseInterface $response) { + foreach ($response->getHeader('X-Influxdb-Version') as $version) { + return $this->version = $version; + } + + return null; + }); + } + + public function listDatabases() + { + return $this->query('SHOW DATABASES')->then(function ($result) { + return InfluxDbQueryResult::extractColumn($result); + }); + } + + public function createDatabase($name) + { + return $this->query('CREATE DATABASE ' . Escape::fieldValue($name))->then(function ($result) { + return $result; + }); + } + + /** + * only since vX + */ + public function getHealth() + { + // Works without Auth + return $this->getUrl('health'); + } + + protected function query($query) + { + if (is_array($query)) { + $sendQueries = \array_values($query); + } else { + $sendQueries = [$query]; + } + if (empty($query)) { + throw new \InvalidArgumentException('Cannot run no query'); + } + + if (preg_match('/^(SELECT|SHOW|ALTER|CREATE|DELETE|DROP|GRANT|KILL|REVOKE) /', $sendQueries[0], $match)) { + $queryType = $match[1]; + } else { + throw new \InvalidArgumentException('Unable to detect query type: ' . $sendQueries[0]); + } + if ($queryType === 'SHOW') { + $queryType = 'GET'; + } elseif ($queryType === 'SELECT') { + if (strpos($sendQueries[0], ' INTO ') === false) { + $queryType = 'POST'; + } else { + $queryType = 'GET'; + } + } else { + $queryType = 'POST'; + } + $prefix = ''; + + // TODO: Temporarily disabled, had problems with POST params in the body + if ($queryType === 'xPOST') { + $headers = ['Content-Type' => 'x-www-form-urlencoded']; + $body = \http_build_query(['q' => implode(';', $sendQueries)]); + $urlParams = []; + $promise = $this->curl->post( + $this->url("{$prefix}query", $urlParams), + $this->getRequestHeaders() + $headers, + $body + ); + } else { + $urlParams = ['q' => implode(';', $sendQueries)]; + $promise = $this->curl->get( + $this->url("{$prefix}query", $urlParams), + $this->getRequestHeaders() + ); + } + + /** @var Promise $promise */ + return $promise->then(function (ResponseInterface $response) use ($sendQueries, $query) { + $body = $response->getBody(); + if (! ($response->getStatusCode() < 300)) { + throw new \Exception($response->getReasonPhrase()); + } + if (preg_match('#^application/json#', \current($response->getHeader('content-type')))) { + $decoded = JsonString::decode((string) $body); + } else { + throw new \RuntimeException(\sprintf( + 'JSON response expected, got %s: %s', + current($response->getHeader('content-type')), + $body + )); + } + $results = []; + foreach ($decoded->results as $result) { + if (isset($result->series)) { + $results[$result->statement_id] = $result->series[0]; + } elseif (isset($result->error)) { + $results[$result->statement_id] = new \Exception('InfluxDB error: ' . $result->error); + } else { + $results[$result->statement_id] = null; + } + } + if (\count($results) !== \count($sendQueries)) { + throw new \InvalidArgumentException(\sprintf( + 'Sent %d statements, but got %d results', + \count($sendQueries), + \count($results) + )); + } + + if (is_array($query)) { + return \array_combine(\array_keys($query), $results); + } else { + return $results[0]; + } + }); + } + + /** + * @param string $dbName + * @param DataPoint[] $dataPoints + * @param string|null $precision ns,u,ms,s,m,h + * @return \React\Promise\Promise + */ + public function writeDataPoints($dbName, array $dataPoints, $precision = null) + { + $body = gzencode(\implode($dataPoints), 6); + $params = ['db' => $dbName]; + if ($precision !== null) { + $params['precision'] = $precision; + } + $headers = [ + 'X-Request-Id' => Uuid::uuid4()->toString(), + 'Content-Encoding' => 'gzip', + 'Content-Length' => strlen($body), + ]; + // params['rp'] = $retentionPolicy + /** @var Promise $promise */ + return $this->curl->post( + $this->url('write', $params), + $this->getRequestHeaders() + $headers, + $body, + $this->getDefaultCurlOptions() + ); + } + + protected function getDefaultCurlOptions() + { + return [ + // Hint: avoid 100/Continue + CURLOPT_HTTPHEADER => [ + 'Expect:', + ] + ]; + } + + protected function getRequestHeaders() + { + $headers = [ + 'User-Agent' => static::USER_AGENT, + ]; + if ($this->username !== null) { + $headers['Authorization'] = 'Basic ' + . \base64_encode($this->username . ':' . $this->password); + } + + return $headers; + } + + protected function get($url, $params = null) + { + return $this->curl->get( + $this->url($url, $params), + $this->getRequestHeaders() + ); + } + + protected function getRaw($url, $params = null) + { + /** @var Promise $promise */ + $promise = $this + ->get($url, $params) + ->then(function (ResponseInterface $response) { + return (string) $response->getBody(); + }); + + return $promise; + } + + protected function postRaw($url, $body, $headers = [], $urlParams = []) + { + /** @var Promise $promise */ + $promise = $this->curl->post( + $this->url($url, $urlParams), + $this->getRequestHeaders() + $headers + [ + 'Content-Type' => 'application/json' + ], + $body + )->then(function (ResponseInterface $response) { + return (string) $response->getBody(); + }); + + return $promise; + } + + protected function getUrl($url, $params = null) + { + return $this->getRaw($url, $params)->then(function ($raw) { + return JsonString::decode((string) $raw); + }); + } + + protected function postUrl($url, $body, $headers = [], $urlParams = []) + { + return $this->postRaw($url, JsonString::encode($body), $headers, $urlParams)->then(function ($raw) { + return JsonString::decode((string) $raw); + }); + } + + protected function url($path, $params = []) + { + $url = $this->baseUrl . "/$path"; + if (! empty($params)) { + $url .= '?' . \http_build_query($params); + } + + return $url; + } +} diff --git a/vendor/gipfl/influxdb/src/InfluxDbConnectionV2.php b/vendor/gipfl/influxdb/src/InfluxDbConnectionV2.php new file mode 100644 index 0000000..d244786 --- /dev/null +++ b/vendor/gipfl/influxdb/src/InfluxDbConnectionV2.php @@ -0,0 +1,270 @@ +<?php + +namespace gipfl\InfluxDb; + +use gipfl\Curl\CurlAsync; +use gipfl\Json\JsonString; +use Psr\Http\Message\ResponseInterface; +use Ramsey\Uuid\Uuid; +use React\EventLoop\LoopInterface; +use React\Promise\Promise; + +class InfluxDbConnectionV2 implements InfluxDbConnection +{ + const API_VERSION = 'v2'; + + const USER_AGENT = 'gipfl-InfluxDB/0.5'; + + /** @var CurlAsync */ + protected $curl; + + /** @var string */ + protected $baseUrl; + + /** @var string|null */ + protected $token; + + /** @var string|null */ + protected $org; + + /** + * AsyncInfluxDbWriter constructor. + * @param $baseUrl string InfluxDB base URL + * @param LoopInterface $loop + */ + public function __construct(CurlAsync $curl, $baseUrl, $org, $token) + { + $this->baseUrl = $baseUrl; + $this->setOrg($org); + $this->setToken($token); + $this->curl = $curl; + } + + /** + * @param string|null $token + * @return $this + */ + public function setToken($token) + { + $this->token = $token; + + return $this; + } + + /** + * @param string|null $org + * @return $this + */ + public function setOrg($org) + { + $this->org = $org; + + return $this; + } + + public function ping($verbose = false) + { + $params = []; + if ($verbose) { + $params['verbose'] = 'true'; + } + return $this->getUrl('ping', $params); + } + + public function getVersion() + { + return $this->getHealth()->then(function ($result) { + return $result->version; + }); + } + + public function getMyOrgId() + { + return $this->getUrl('api/v2/orgs', ['org' => urlencode($this->org)])->then(function ($result) { + foreach ($result->orgs as $org) { + if ($org->name === $this->org) { + return $org->id; + } + } + + throw new \RuntimeException('Org "' . $this->org . '" not found'); + }); + } + + public function listDatabases() + { + // ->links->self = "/api/v2/buckets?descending=false\u0026limit=2\u0026offset=0" + // ->links->next = "next": "/api/v2/buckets?descending=false\u0026limit=2\u0026offset=2" + // 100 -> maxlimit + return $this->getUrl('api/v2/buckets', ['limit' => 100])->then(function ($result) { + $list = []; + foreach ($result->buckets as $bucket) { + $list[] = $bucket->name; + } + + return $list; + }); + } + + public function createDatabase($name) + { + return $this->getMyOrgId()->then(function ($orgId) use ($name) { + return $this->postUrl('api/v2/buckets', [ + 'orgID' => $orgId, + 'name' => $name, + 'retentionRules' => [(object) [ + 'type' => 'expire', + 'everySeconds' => 86400 * 7, + ]] + ]); + })->then(function ($result) { + return $result; + }); + } + + public function getHealth() + { + // Works without Auth + return $this->getUrl('health'); + } + + /** + * TODO: unfinished + * @param $query + * @throws \gipfl\Json\JsonEncodeException + */ + protected function query($query) + { + $prefix = "api/v2/"; + $headers = ['Content-Type' => 'application/json']; + $body = JsonString::encode(['query' => $query]); + $urlParams = ['org' => $this->org]; + } + + /** + * @param string $dbName + * @param DataPoint[] $dataPoints + * @param string|null $precision ns,u,ms,s,m,h + * @return \React\Promise\Promise + */ + public function writeDataPoints($dbName, array $dataPoints, $precision = null) + { + $body = gzencode(\implode($dataPoints), 6); + $params = [ + 'org' => $this->org, + 'bucket' => $dbName, + // TODO: Figure out, whether 2.0.0 also supports bucket. If so, drop db + 'db' => $dbName, + ]; + $headers = [ + 'X-Request-Id' => Uuid::uuid4()->toString(), + 'Content-Encoding' => 'gzip', + 'Content-Length' => strlen($body), + ]; + if ($precision !== null) { + $params['precision'] = $precision; + } + // params['rp'] = $retentionPolicy + return $this->curl->post( + $this->url('api/v2/write', $params), + $this->defaultHeaders() + $headers, + $body, + $this->getDefaultCurlOptions() + ); + } + + protected function getDefaultCurlOptions() + { + return [ + // Hint: avoid 100/Continue + CURLOPT_HTTPHEADER => [ + 'Expect:', + ] + ]; + } + + protected function defaultHeaders() + { + $headers = [ + 'User-Agent' => static::USER_AGENT, + ]; + if ($this->token) { + $headers['Authorization'] = 'Token ' . $this->token; + } + + return $headers; + } + + protected function get($url, $params = null) + { + return $this->curl->get( + $this->url($url, $params), + $this->defaultHeaders() + ); + } + + protected function getRaw($url, $params = null) + { + /** @var Promise $promise */ + $promise = $this + ->get($url, $params) + ->then(function (ResponseInterface $response) { + if ($response->getStatusCode() < 300) { + return (string) $response->getBody(); + } else { + try { + $body = JsonString::decode($response->getBody()); + } catch (\Exception $e) { + throw new \Exception($response->getReasonPhrase()); + } + if (isset($body->message)) { + throw new \Exception($body->message); + } else { + throw new \Exception($response->getReasonPhrase()); + } + } + }); + + return $promise; + } + + protected function postRaw($url, $body, $headers = [], $urlParams = []) + { + /** @var Promise $promise */ + $promise = $this->curl->post( + $this->url($url, $urlParams), + $this->defaultHeaders() + $headers + [ + 'Content-Type' => 'application/json' + ], + $body + )->then(function (ResponseInterface $response) { + return (string) $response->getBody(); + }); + + return $promise; + } + + protected function getUrl($url, $params = null) + { + return $this->getRaw($url, $params)->then(function ($raw) { + return JsonString::decode((string) $raw); + }); + } + + protected function postUrl($url, $body, $headers = [], $urlParams = []) + { + return $this->postRaw($url, JsonString::encode($body), $headers, $urlParams)->then(function ($raw) { + return JsonString::decode((string) $raw); + }); + } + + protected function url($path, $params = []) + { + $url = $this->baseUrl . "/$path"; + if (! empty($params)) { + $url .= '?' . \http_build_query($params); + } + + return $url; + } +} diff --git a/vendor/gipfl/influxdb/src/InfluxDbQueryResult.php b/vendor/gipfl/influxdb/src/InfluxDbQueryResult.php new file mode 100644 index 0000000..0ca6fd1 --- /dev/null +++ b/vendor/gipfl/influxdb/src/InfluxDbQueryResult.php @@ -0,0 +1,65 @@ +<?php + +namespace gipfl\InfluxDb; + +use InvalidArgumentException; + +class InfluxDbQueryResult +{ + public static function extractColumn($result, $idx = 0) + { + if (! isset($result->columns)) { + print_r($result); + exit; + } + $idx = static::getNumericColumn($idx, $result->columns); + $column = []; + foreach ($result->values as $row) { + $column[] = $row[$idx]; + } + + return $column; + } + + protected static function getNumericColumn($name, $cols) + { + if (\is_int($name)) { + if (isset($cols[$name])) { + return $name; + } + } + if (\is_string($name)) { + foreach ($cols as $idx => $alias) { + if ($name === $alias) { + return $idx; + } + } + } + + throw new InvalidArgumentException("There is no '$name' column in the result"); + } + + protected static function extractPairs($result, $keyColumn = 0, $valueColumn = 1) + { + $keyColumn = static::getNumericColumn($keyColumn, $result->columns); + $valueColumn = static::getNumericColumn($valueColumn, $result->columns); + $pairs = []; + foreach ($result->values as $row) { + $pairs[$row[$keyColumn]] = $row[$valueColumn]; + } + + return $pairs; + } + + protected static function transformResultsTable($table) + { + // $table->name = 'databases' + $cols = $table->columns; + $values = []; + foreach ($table->values as $row) { + $values[] = (object) \array_combine($cols, $row); + } + + return $values; + } +} diff --git a/vendor/gipfl/influxdb/src/LineProtocol.php b/vendor/gipfl/influxdb/src/LineProtocol.php new file mode 100644 index 0000000..b3b5f4a --- /dev/null +++ b/vendor/gipfl/influxdb/src/LineProtocol.php @@ -0,0 +1,63 @@ +<?php + +namespace gipfl\InfluxDb; + +use function ksort; +use function strlen; + +abstract class LineProtocol +{ + public static function renderMeasurement($measurement, $tags = [], $fields = [], $timestamp = null) + { + return Escape::measurement($measurement) + . static::renderTags($tags) + . static::renderFields($fields) + . static::renderTimeStamp($timestamp) + . "\n"; + } + + public static function renderTags($tags) + { + ksort($tags); + $string = ''; + foreach ($tags as $key => $value) { + if ($value === null || strlen($value) === 0) { + continue; + } + $string .= ',' . static::renderTag($key, $value); + } + + return $string; + } + + public static function renderFields($fields) + { + $string = ''; + foreach ($fields as $key => $value) { + $string .= ',' . static::renderField($key, $value); + } + $string[0] = ' '; + + return $string; + } + + public static function renderTimeStamp($timestamp) + { + if ($timestamp === null) { + return ''; + } else { + return " $timestamp"; + } + } + + public static function renderTag($key, $value) + { + return Escape::key($key) . '=' . Escape::tagValue($value); + } + + public static function renderField($key, $value) + { + + return Escape::key($key) . '=' . Escape::fieldValue($value); + } +} diff --git a/vendor/gipfl/json/composer.json b/vendor/gipfl/json/composer.json new file mode 100644 index 0000000..7e3f93c --- /dev/null +++ b/vendor/gipfl/json/composer.json @@ -0,0 +1,20 @@ +{ + "name": "gipfl/json", + "description": "Simple JSON-related helper classes and interfaces", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "gipfl\\Json\\": "src/" + } + }, + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "require": { + "ext-json": "*" + } +} diff --git a/vendor/gipfl/json/src/JsonDecodeException.php b/vendor/gipfl/json/src/JsonDecodeException.php new file mode 100644 index 0000000..cd19aa7 --- /dev/null +++ b/vendor/gipfl/json/src/JsonDecodeException.php @@ -0,0 +1,7 @@ +<?php + +namespace gipfl\Json; + +class JsonDecodeException extends JsonException +{ +} diff --git a/vendor/gipfl/json/src/JsonEncodeException.php b/vendor/gipfl/json/src/JsonEncodeException.php new file mode 100644 index 0000000..e9fcc5f --- /dev/null +++ b/vendor/gipfl/json/src/JsonEncodeException.php @@ -0,0 +1,7 @@ +<?php + +namespace gipfl\Json; + +class JsonEncodeException extends JsonException +{ +} diff --git a/vendor/gipfl/json/src/JsonException.php b/vendor/gipfl/json/src/JsonException.php new file mode 100644 index 0000000..e7b5f36 --- /dev/null +++ b/vendor/gipfl/json/src/JsonException.php @@ -0,0 +1,55 @@ +<?php + +namespace gipfl\Json; + +use Exception; + +class JsonException extends Exception +{ + public static function forLastJsonError($msg = null) + { + if ($msg === null) { + return new static(static::getJsonErrorMessage(\json_last_error())); + } else { + return new static($msg . ': ' . static::getJsonErrorMessage(\json_last_error())); + } + } + + public static function getJsonErrorMessage($code) + { + $map = [ + JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded', + JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded', + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', + JSON_ERROR_SYNTAX => 'JSON Syntax error', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' + ]; + if (\array_key_exists($code, $map)) { + return $map[$code]; + } + + if (PHP_VERSION_ID >= 50500) { + $map = [ + JSON_ERROR_RECURSION => 'One or more recursive references in the value to be encoded', + JSON_ERROR_INF_OR_NAN => 'One or more NAN or INF values in the value to be encoded', + JSON_ERROR_UNSUPPORTED_TYPE => 'A value of a type that cannot be encoded was given', + ]; + if (\array_key_exists($code, $map)) { + return $map[$code]; + } + } + + if (PHP_VERSION_ID >= 70000) { + $map = [ + JSON_ERROR_INVALID_PROPERTY_NAME => 'A property name that cannot be encoded was given', + JSON_ERROR_UTF16 => 'Malformed UTF-16 characters, possibly incorrectly encoded', + ]; + + if (\array_key_exists($code, $map)) { + return $map[$code]; + } + } + + return 'An error occured when parsing a JSON string'; + } +} diff --git a/vendor/gipfl/json/src/JsonSerialization.php b/vendor/gipfl/json/src/JsonSerialization.php new file mode 100644 index 0000000..f5b058e --- /dev/null +++ b/vendor/gipfl/json/src/JsonSerialization.php @@ -0,0 +1,14 @@ +<?php + +namespace gipfl\Json; + +use JsonSerializable; + +interface JsonSerialization extends JsonSerializable +{ + /** + * @param mixed $any + * @return static + */ + public static function fromSerialization($any); +} diff --git a/vendor/gipfl/json/src/JsonString.php b/vendor/gipfl/json/src/JsonString.php new file mode 100644 index 0000000..b9e22b6 --- /dev/null +++ b/vendor/gipfl/json/src/JsonString.php @@ -0,0 +1,68 @@ +<?php + +namespace gipfl\Json; + +use function json_decode; +use function json_encode; +use function json_last_error; + +class JsonString +{ + const DEFAULT_FLAGS = JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; + + /** + * Encode with well-known flags, as we require the result to be reproducible + * + * @param $mixed + * @param int|null $flags + * @return string + * @throws JsonEncodeException + */ + public static function encode($mixed, $flags = null) + { + if ($flags === null) { + $flags = self::DEFAULT_FLAGS; + } else { + $flags = self::DEFAULT_FLAGS | $flags; + } + $result = json_encode($mixed, $flags); + + if ($result === false && json_last_error() !== JSON_ERROR_NONE) { + throw JsonEncodeException::forLastJsonError(); + } + + return $result; + } + + /** + * Decode the given JSON string and make sure we get a meaningful Exception + * + * @param string $string + * @return mixed + * @throws JsonDecodeException + */ + public static function decode($string) + { + $result = json_decode($string); + + if ($result === null && json_last_error() !== JSON_ERROR_NONE) { + throw JsonDecodeException::forLastJsonError(); + } + + return $result; + } + + /** + * @param $string + * @return ?string + * @throws JsonDecodeException + */ + public static function decodeOptional($string) + { + if ($string === null) { + return null; + } + + return static::decode($string); + } +} diff --git a/vendor/gipfl/json/src/SerializationHelper.php b/vendor/gipfl/json/src/SerializationHelper.php new file mode 100644 index 0000000..0714e30 --- /dev/null +++ b/vendor/gipfl/json/src/SerializationHelper.php @@ -0,0 +1,55 @@ +<?php + +namespace gipfl\Json; + +use InvalidArgumentException; +use JsonSerializable; +use stdClass; + +class SerializationHelper +{ + /** + * TODO: Check whether json_encode() is faster + * + * @param mixed $value + * @return bool + */ + public static function assertSerializableValue($value) + { + if ($value === null || is_scalar($value)) { + return true; + } + if (is_object($value)) { + if ($value instanceof JsonSerializable) { + return true; + } + + if ($value instanceof stdClass) { + foreach ((array) $value as $val) { + static::assertSerializableValue($val); + } + + return true; + } + } + + if (is_array($value)) { + foreach ($value as $val) { + static::assertSerializableValue($val); + } + + return true; + } + + throw new InvalidArgumentException('Serializable value expected, got ' . static::getPhpType($value)); + } + + public static function getPhpType($var) + { + if (is_object($var)) { + return get_class($var); + } + + return gettype($var); + } +} diff --git a/vendor/gipfl/linux-health/composer.json b/vendor/gipfl/linux-health/composer.json new file mode 100644 index 0000000..d162cc8 --- /dev/null +++ b/vendor/gipfl/linux-health/composer.json @@ -0,0 +1,23 @@ +{ + "name": "gipfl/linux-health", + "description": "Various little Linux Health based classes", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\LinuxHealth\\": "src" + } + }, + "require": { + "php": ">=5.4.0" + } +} diff --git a/vendor/gipfl/linux-health/src/Cpu.php b/vendor/gipfl/linux-health/src/Cpu.php new file mode 100644 index 0000000..43f2e50 --- /dev/null +++ b/vendor/gipfl/linux-health/src/Cpu.php @@ -0,0 +1,59 @@ +<?php + +namespace gipfl\LinuxHealth; + +class Cpu +{ + public static function getCounters($procFile = '/proc/stat') + { + $info = []; + $cpus = []; + + $cpuKeys = [ // From 'man proc': + 'user', // Time spent in user mode. + 'nice', // Time spent in user mode with low priority (nice). + 'system', // Time spent in system mode. + 'idle', // Time spent in the idle task. + // This value should be USER_HZ times the second entry in the + // /proc/uptime pseudo-file. + 'iowait', // Time waiting for I/O to complete. (Linux >= 2.5.41) + 'irq', // Time servicing interrupts. (Linux >= 2.6.0-test4) + 'softirq', // Time servicing softirqs. (Linux >= 2.6.0-test4) + 'steal', // Stolen time, which is the time spent in other operating + // systems when running in a virtualized environment + // (Linux >= 2.6.11) + 'guest', // Time spent running a virtual CPU for guest operating systems + // under the control of the Linux kernel. (Linux >= 2.6.24) + 'guest_nice' // Time spent running a niced guest (virtual CPU for guest + // operating systems under the control of the Linux kernel). + // (Linux >= 2.6.33) + ]; + + // TODO: + // ctxt 891299797 -> The number of context switches that the system underwent + // btime 1540828526 -> boot time, in seconds since the Epoch + // processes 2079015 -> Number of forks since boot + // procs_running 6 -> Number of processes in runnable state + // procs_blocked 0 -> Number of processes blocked waiting for I/O to complete + + foreach (file($procFile, FILE_IGNORE_NEW_LINES) as $line) { + $parts = preg_split('/\s+/', $line); + $key = array_shift($parts); + if (substr($key, 0, 3) === 'cpu') { + // TODO: handle count mismatch + $cpus[$key] = array_combine( + array_slice($cpuKeys, 0, count($parts), true), + $parts + ); + + for ($i = count($cpus[$key]) - 1; $i < count($cpuKeys); $i++) { + $cpus[$key][$cpuKeys[$i]] = 0; + } + } else { + $info[$key] = $parts; + } + } + + return $cpus; + } +} diff --git a/vendor/gipfl/linux-health/src/Memory.php b/vendor/gipfl/linux-health/src/Memory.php new file mode 100644 index 0000000..0c6f197 --- /dev/null +++ b/vendor/gipfl/linux-health/src/Memory.php @@ -0,0 +1,52 @@ +<?php + +namespace gipfl\LinuxHealth; + +class Memory +{ + protected static $pageSize; + + public static function getUsageForPid($pid) + { + $pid = (int) $pid; + $content = @file_get_contents("/proc/$pid/statm"); + if ($content === false) { + return false; + } + + $pageSize = static::getPageSize(); + if ($pageSize === null) { + return false; + } + $parts = explode(' ', $content); + + return (object) [ + 'size' => $pageSize * (int) $parts[0], + 'rss' => $pageSize * (int) $parts[1], + 'shared' => $pageSize * (int) $parts[3], + ]; + } + + /** + * @return int + */ + public static function getPageSize() + { + if (self::$pageSize === null) { + $output = trim(`getconf PAGESIZE 2>&1`); + if (strlen($output)) { + self::$pageSize = (int) $output; + } + } + + return self::$pageSize; + } + + /** + * @param int $pageSize + */ + public static function setPageSize($pageSize) + { + self::$pageSize = (int) $pageSize; + } +} diff --git a/vendor/gipfl/linux-health/src/Network.php b/vendor/gipfl/linux-health/src/Network.php new file mode 100644 index 0000000..e0bad7d --- /dev/null +++ b/vendor/gipfl/linux-health/src/Network.php @@ -0,0 +1,36 @@ +<?php + +namespace gipfl\LinuxHealth; + +class Network +{ + public static function getInterfaceCounters($procFile = '/proc/net/dev') + { + // Header looks like this: + // Inter-| Receive | Transmit + // face |bytes packets errs drop fifo frame compressed multicast|bytes packets + // (...from above line) errs drop fifo colls carrier compressed + + $lines = \file($procFile, FILE_IGNORE_NEW_LINES); + \array_shift($lines); + $headers = preg_split('/\|/', array_shift($lines)); + $rxHeaders = preg_split('/\s+/', $headers[1]); + $txHeaders = preg_split('/\s+/', $headers[2]); + + $headers = []; + foreach ($rxHeaders as $rx) { + $headers[] = 'rx' . ucfirst($rx); + } + foreach ($txHeaders as $tx) { + $headers[] = 'tx' . ucfirst($tx); + } + $interfaces = []; + foreach ($lines as $line) { + $parts = preg_split('/\s+|\|/', trim($line)); + $ifName = rtrim(array_shift($parts), ':'); + $interfaces[$ifName] = (object) array_combine($headers, $parts); + } + + return $interfaces; + } +} diff --git a/vendor/gipfl/log/LICENSE b/vendor/gipfl/log/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/log/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/log/composer.json b/vendor/gipfl/log/composer.json new file mode 100644 index 0000000..c300158 --- /dev/null +++ b/vendor/gipfl/log/composer.json @@ -0,0 +1,31 @@ +{ + "name": "gipfl/log", + "description": "Lightweight PSR-3 compatible logger", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Log\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "psr/log": "^1", + "ext-iconv": "*" + }, + "require-dev": { + "gipfl/protocol-jsonrpc": ">=0.2", + "gipfl/systemd": ">=0.3" + }, + "suggest": { + } +} diff --git a/vendor/gipfl/log/src/AdditionalContextLogger.php b/vendor/gipfl/log/src/AdditionalContextLogger.php new file mode 100644 index 0000000..92891c5 --- /dev/null +++ b/vendor/gipfl/log/src/AdditionalContextLogger.php @@ -0,0 +1,25 @@ +<?php + +namespace gipfl\Log; + +use Psr\Log\LoggerInterface; + +class AdditionalContextLogger extends Logger +{ + /** @var array */ + protected $context; + + /** @var LoggerInterface */ + protected $wrappedLogger; + + public function __construct(array $context, LoggerInterface $logger) + { + $this->context = $context; + $this->wrappedLogger = $logger; + } + + public function log($level, $message, array $context = []) + { + $this->wrappedLogger->log($level, $message, $context + $this->context); + } +} diff --git a/vendor/gipfl/log/src/DummyLogger.php b/vendor/gipfl/log/src/DummyLogger.php new file mode 100644 index 0000000..3e7b0c4 --- /dev/null +++ b/vendor/gipfl/log/src/DummyLogger.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\Log; + +use Psr\Log\LoggerInterface; + +class DummyLogger implements LoggerInterface +{ + public function emergency($message, array $context = []) + { + } + + public function alert($message, array $context = []) + { + } + + public function critical($message, array $context = []) + { + } + + public function error($message, array $context = []) + { + } + + public function warning($message, array $context = []) + { + } + + public function notice($message, array $context = []) + { + } + + public function info($message, array $context = []) + { + } + + public function debug($message, array $context = []) + { + } + + public function log($level, $message, array $context = []) + { + } +} diff --git a/vendor/gipfl/log/src/Filter/LogLevelFilter.php b/vendor/gipfl/log/src/Filter/LogLevelFilter.php new file mode 100644 index 0000000..f26773a --- /dev/null +++ b/vendor/gipfl/log/src/Filter/LogLevelFilter.php @@ -0,0 +1,39 @@ +<?php + +namespace gipfl\Log\Filter; + +use gipfl\Log\LogFilter; +use gipfl\Log\LogLevel; + +class LogLevelFilter implements LogFilter +{ + /** @var int */ + protected $level; + + /** + * @param string $level + */ + public function __construct($level) + { + $this->level = LogLevel::mapNameToNumeric($level); + } + + /** + * @param string $level + * @param string $message + * @param array $context + * @return bool + */ + public function wants($level, $message, $context = []) + { + return LogLevel::mapNameToNumeric($level) <= $this->level; + } + + /** + * @return string + */ + public function getLevel() + { + return LogLevel::mapNumericToName($this->level); + } +} diff --git a/vendor/gipfl/log/src/Formatter/StdOutFormatter.php b/vendor/gipfl/log/src/Formatter/StdOutFormatter.php new file mode 100644 index 0000000..363a0b2 --- /dev/null +++ b/vendor/gipfl/log/src/Formatter/StdOutFormatter.php @@ -0,0 +1,34 @@ +<?php + +namespace gipfl\Log\Formatter; + +use gipfl\Log\LogFormatter; +use function date; +use function microtime; + +class StdOutFormatter implements LogFormatter +{ + protected $dateFormat = 'Y-m-d H:i:s'; + + protected $showTimestamp = true; + + public function format($level, $message, $context = []) + { + // TODO: replace placeholders! + return $this->renderDatePrefix() . sprintf($message, $context); + } + + protected function renderDatePrefix() + { + if ($this->showTimestamp) { + return date($this->dateFormat, microtime(true)); + } + + return ''; + } + + public function setShowTimestamp($show = true) + { + $this->showTimestamp = $show; + } +} diff --git a/vendor/gipfl/log/src/IcingaWeb/IcingaLogger.php b/vendor/gipfl/log/src/IcingaWeb/IcingaLogger.php new file mode 100644 index 0000000..d5de5e9 --- /dev/null +++ b/vendor/gipfl/log/src/IcingaWeb/IcingaLogger.php @@ -0,0 +1,29 @@ +<?php + +namespace gipfl\Log\IcingaWeb; + +use Icinga\Application\Logger as IcingaApplicationLogger; +use Icinga\Exception\ConfigurationError; +use Psr\Log\LoggerInterface; + +class IcingaLogger extends IcingaApplicationLogger +{ + public static function replace(LoggerInterface $logger) + { + static::replaceRunningInstance(new LoggerLogWriter($logger)); + } + + public static function replaceRunningInstance(LoggerLogWriter $writer, $level = null) + { + try { + $instance = static::$instance; + if ($level !== null) { + $instance->setLevel($level); + } + + $instance->writer = $writer; + } catch (ConfigurationError $e) { + static::$instance->error($e->getMessage()); + } + } +} diff --git a/vendor/gipfl/log/src/IcingaWeb/LoggerLogWriter.php b/vendor/gipfl/log/src/IcingaWeb/LoggerLogWriter.php new file mode 100644 index 0000000..aea4da4 --- /dev/null +++ b/vendor/gipfl/log/src/IcingaWeb/LoggerLogWriter.php @@ -0,0 +1,32 @@ +<?php + +namespace gipfl\Log\IcingaWeb; + +use Icinga\Application\Logger as IcingaApplicationLogger; +use Icinga\Application\Logger\LogWriter; +use Icinga\Data\ConfigObject; +use Psr\Log\LoggerInterface; + +class LoggerLogWriter extends LogWriter +{ + protected $logger; + + protected static $severityMap = [ + IcingaApplicationLogger::DEBUG => 'debug', + IcingaApplicationLogger::INFO => 'info', + IcingaApplicationLogger::WARNING => 'warning', + IcingaApplicationLogger::ERROR => 'error', + ]; + + public function __construct(LoggerInterface $logger) + { + parent::__construct(new ConfigObject([])); + $this->logger = $logger; + } + + public function log($severity, $message) + { + $severity = static::$severityMap[$severity]; + $this->logger->$severity($message); + } +} diff --git a/vendor/gipfl/log/src/LogFilter.php b/vendor/gipfl/log/src/LogFilter.php new file mode 100644 index 0000000..79749f7 --- /dev/null +++ b/vendor/gipfl/log/src/LogFilter.php @@ -0,0 +1,14 @@ +<?php + +namespace gipfl\Log; + +interface LogFilter +{ + /** + * @param string $level + * @param string $message + * @param array $context + * @return bool + */ + public function wants($level, $message, $context = []); +} diff --git a/vendor/gipfl/log/src/LogFormatter.php b/vendor/gipfl/log/src/LogFormatter.php new file mode 100644 index 0000000..e6529dc --- /dev/null +++ b/vendor/gipfl/log/src/LogFormatter.php @@ -0,0 +1,8 @@ +<?php + +namespace gipfl\Log; + +interface LogFormatter +{ + public function format($level, $message, $context = []); +} diff --git a/vendor/gipfl/log/src/LogLevel.php b/vendor/gipfl/log/src/LogLevel.php new file mode 100644 index 0000000..599420e --- /dev/null +++ b/vendor/gipfl/log/src/LogLevel.php @@ -0,0 +1,66 @@ +<?php + +namespace gipfl\Log; + +use InvalidArgumentException; +use Psr\Log\LogLevel as PsrLogLevel; + +class LogLevel extends PsrLogLevel +{ + const LEVEL_EMERGENCY = 0; + const LEVEL_ALERT = 1; + const LEVEL_CRITICAL = 2; + const LEVEL_ERROR = 3; + const LEVEL_WARNING = 4; + const LEVEL_NOTICE = 5; + const LEVEL_INFO = 6; + const LEVEL_DEBUG = 7; + + const MAP_NAME_TO_LEVEL = [ + self::EMERGENCY => self::LEVEL_EMERGENCY, + self::ALERT => self::LEVEL_ALERT, + self::CRITICAL => self::LEVEL_CRITICAL, + self::ERROR => self::LEVEL_ERROR, + self::WARNING => self::LEVEL_WARNING, + self::NOTICE => self::LEVEL_NOTICE, + self::INFO => self::LEVEL_INFO, + self::DEBUG => self::LEVEL_DEBUG, + ]; + + const MAP_LEVEL_TO_NAME = [ + self::LEVEL_EMERGENCY => self::EMERGENCY, + self::LEVEL_ALERT => self::ALERT, + self::LEVEL_CRITICAL => self::CRITICAL, + self::LEVEL_ERROR => self::ERROR, + self::LEVEL_WARNING => self::WARNING, + self::LEVEL_NOTICE => self::NOTICE, + self::LEVEL_INFO => self::INFO, + self::LEVEL_DEBUG => self::DEBUG, + ]; + + /** + * @param string $name + * @return int + */ + public static function mapNameToNumeric($name) + { + if (array_key_exists($name, static::MAP_NAME_TO_LEVEL)) { + return static::MAP_NAME_TO_LEVEL[$name]; + } + + throw new InvalidArgumentException("$name is not a valid log level name"); + } + + /** + * @param int $number + * @return string + */ + public static function mapNumericToName($number) + { + if (array_key_exists($number, static::MAP_LEVEL_TO_NAME)) { + return static::MAP_LEVEL_TO_NAME[$number]; + } + + throw new InvalidArgumentException("$number is not a valid numeric log level"); + } +} diff --git a/vendor/gipfl/log/src/LogWriter.php b/vendor/gipfl/log/src/LogWriter.php new file mode 100644 index 0000000..8b91d5c --- /dev/null +++ b/vendor/gipfl/log/src/LogWriter.php @@ -0,0 +1,8 @@ +<?php + +namespace gipfl\Log; + +interface LogWriter +{ + public function write($level, $message); +} diff --git a/vendor/gipfl/log/src/LogWriterWithContext.php b/vendor/gipfl/log/src/LogWriterWithContext.php new file mode 100644 index 0000000..4372eda --- /dev/null +++ b/vendor/gipfl/log/src/LogWriterWithContext.php @@ -0,0 +1,8 @@ +<?php + +namespace gipfl\Log; + +interface LogWriterWithContext extends LogWriter +{ + public function write($level, $message, $context = []); +} diff --git a/vendor/gipfl/log/src/Logger.php b/vendor/gipfl/log/src/Logger.php new file mode 100644 index 0000000..1cbeb78 --- /dev/null +++ b/vendor/gipfl/log/src/Logger.php @@ -0,0 +1,169 @@ +<?php + +namespace gipfl\Log; + +use Psr\Log\LoggerInterface; +use function array_values; +use function spl_object_hash; + +class Logger implements LoggerInterface +{ + /** @deprecated please use LogLevel::LEVEL_EMERGENCY */ + const LEVEL_EMERGENCY = LogLevel::LEVEL_EMERGENCY; + /** @deprecated please use LogLevel::LEVEL_ALERT */ + const LEVEL_ALERT = LogLevel::LEVEL_ALERT; + /** @deprecated please use LogLevel::LEVEL_CRITICAL */ + const LEVEL_CRITICAL = LogLevel::LEVEL_CRITICAL; + /** @deprecated please use LogLevel::LEVEL_ERROR */ + const LEVEL_ERROR = LogLevel::LEVEL_ERROR; + /** @deprecated please use LogLevel::LEVEL_WARNING */ + const LEVEL_WARNING = LogLevel::LEVEL_WARNING; + /** @deprecated please use LogLevel::LEVEL_NOTICE */ + const LEVEL_NOTICE = LogLevel::LEVEL_NOTICE; + /** @deprecated please use LogLevel::LEVEL_INFO */ + const LEVEL_INFO = LogLevel::LEVEL_INFO; + /** @deprecated please use LogLevel::LEVEL_DEBUG */ + const LEVEL_DEBUG = LogLevel::LEVEL_DEBUG; + /** @deprecated Please use LogLevel::MAP_NAME_TO_LEVEL */ + const MAP_NAME_TO_LEVEL = LogLevel::MAP_NAME_TO_LEVEL; + + /** @var LogWriter[] */ + protected $writers = []; + + /** @var LogFilter[] */ + protected $filters = []; + + /** + * @param LogWriter $writer + */ + public function addWriter(LogWriter $writer) + { + $this->writers[spl_object_hash($writer)] = $writer; + } + + /** + * @param LogFilter $filter + */ + public function addFilter(LogFilter $filter) + { + $this->filters[spl_object_hash($filter)] = $filter; + } + + /** + * @return LogWriter[] + */ + public function getWriters() + { + return array_values($this->writers); + } + + /** + * @return LogFilter[] + */ + public function getFilters() + { + return array_values($this->filters); + } + + /** + * @param LogWriter $writer + */ + public function removeWriter(LogWriter $writer) + { + unset($this->filters[spl_object_hash($writer)]); + } + + /** + * @param LogFilter $filter + */ + public function removeFilter(LogFilter $filter) + { + unset($this->filters[spl_object_hash($filter)]); + } + + /** + * @deprecated Please use LogLevel::mapNameToNumeric() + */ + public static function mapLogLevel($name) + { + return LogLevel::mapNameToNumeric($name); + } + + public function emergency($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function alert($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function critical($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function error($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function warning($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function notice($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function info($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function debug($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function wants($level, $message, array $context = []) + { + foreach ($this->filters as $filter) { + if (! $filter->wants($level, $message, $context)) { + return false; + } + } + + return true; + } + + public function log($level, $message, array $context = []) + { + if (! $this->wants($level, $message, $context)) { + return; + } + + foreach ($this->writers as $writer) { + if ($writer instanceof LogWriterWithContext) { + $writer->write($level, $message, $context); + } else { + $writer->write($level, $this->formatMessage( + $message, + $context + )); + } + } + } + + protected function formatMessage($message, $context = []) + { + if (empty($context)) { + return $message; + } else { + return \sprintf($message, $context); + } + } +} diff --git a/vendor/gipfl/log/src/PrefixLogger.php b/vendor/gipfl/log/src/PrefixLogger.php new file mode 100644 index 0000000..a7273a2 --- /dev/null +++ b/vendor/gipfl/log/src/PrefixLogger.php @@ -0,0 +1,25 @@ +<?php + +namespace gipfl\Log; + +use Psr\Log\LoggerInterface; + +class PrefixLogger extends Logger +{ + /** @var string */ + protected $prefix; + + /** @var LoggerInterface */ + protected $wrappedLogger; + + public function __construct($prefix, LoggerInterface $logger) + { + $this->prefix = $prefix; + $this->wrappedLogger = $logger; + } + + public function log($level, $message, array $context = []) + { + $this->wrappedLogger->log($level, $this->prefix . $message, $context); + } +} diff --git a/vendor/gipfl/log/src/Writer/JournaldLogger.php b/vendor/gipfl/log/src/Writer/JournaldLogger.php new file mode 100644 index 0000000..b3b0125 --- /dev/null +++ b/vendor/gipfl/log/src/Writer/JournaldLogger.php @@ -0,0 +1,61 @@ +<?php + +namespace gipfl\Log\Writer; + +use gipfl\Log\LogLevel; +use gipfl\Log\LogWriterWithContext; +use gipfl\SystemD\NotificationSocket; +use React\EventLoop\LoopInterface; +use React\Stream\WritableStreamInterface; + +class JournaldLogger implements LogWriterWithContext +{ + const JOURNALD_SOCKET = '/run/systemd/journal/socket'; + + protected $socket; + + protected $extraFields = []; + + /** + * SystemdStdoutWriter constructor. + * @param LoopInterface $loop + * @param WritableStreamInterface|null $stdOut + */ + public function __construct($socket = null) + { + $this->socket = new NotificationSocket($socket ?: self::JOURNALD_SOCKET); + } + + /** + * @param string|null $identifier + * @return $this + */ + public function setIdentifier($identifier) + { + return $this->setExtraField('SYSLOG_IDENTIFIER', $identifier); + } + + /** + * @param string $name + * @param ?string $value + * @return $this + */ + public function setExtraField($name, $value) + { + if ($value === null) { + unset($this->extraFields[$name]); + } else { + $this->extraFields[$name] = (string) $value; + } + + return $this; + } + + public function write($level, $message, $context = []) + { + $this->socket->send([ + 'MESSAGE' => $message, + 'PRIORITY' => LogLevel::mapNameToNumeric($level), + ] + $context + $this->extraFields); + } +} diff --git a/vendor/gipfl/log/src/Writer/JsonRpcConnectionWriter.php b/vendor/gipfl/log/src/Writer/JsonRpcConnectionWriter.php new file mode 100644 index 0000000..e4042e7 --- /dev/null +++ b/vendor/gipfl/log/src/Writer/JsonRpcConnectionWriter.php @@ -0,0 +1,51 @@ +<?php + +namespace gipfl\Log\Writer; + +use gipfl\Log\LogWriterWithContext; +use gipfl\Protocol\JsonRpc\JsonRpcConnection; +use function iconv; +use function microtime; + +class JsonRpcConnectionWriter implements LogWriterWithContext +{ + const DEFAULT_RPC_METHOD = 'logger.log'; + + /** @var JsonRpcConnection */ + protected $connection; + + /** @var string */ + protected $method = self::DEFAULT_RPC_METHOD; + + /** @var array */ + protected $defaultContext; + + /** + * @param JsonRpcConnection $connection + * @param array $defaultContext + */ + public function __construct(JsonRpcConnection $connection, $defaultContext = []) + { + $this->connection = $connection; + $this->defaultContext = $defaultContext; + } + + /** + * @param string $method + */ + public function setMethod($method) + { + $this->method = $method; + } + + public function write($level, $message, $context = []) + { + $message = iconv('UTF-8', 'UTF-8//IGNORE', $message); + $this->connection->notification($this->method, $this->defaultContext + [ + 'level' => $level, + 'timestamp' => microtime(true), + 'message' => $message, + 'context' => $context, + ]); + } +} diff --git a/vendor/gipfl/log/src/Writer/JsonRpcWriter.php b/vendor/gipfl/log/src/Writer/JsonRpcWriter.php new file mode 100644 index 0000000..a2fa505 --- /dev/null +++ b/vendor/gipfl/log/src/Writer/JsonRpcWriter.php @@ -0,0 +1,54 @@ +<?php + +namespace gipfl\Log\Writer; + +use gipfl\Log\LogWriterWithContext; +use gipfl\Protocol\JsonRpc\Connection; +use function iconv; +use function microtime; + +/** + * @deprecated + */ +class JsonRpcWriter implements LogWriterWithContext +{ + const DEFAULT_RPC_METHOD = 'logger.log'; + + /** @var Connection */ + protected $connection; + + /** @var string */ + protected $method = self::DEFAULT_RPC_METHOD; + + /** @var array */ + protected $defaultContext; + + /** + * JsonRpcWriter constructor. + * @param Connection $connection + */ + public function __construct(Connection $connection, $defaultContext = []) + { + $this->connection = $connection; + $this->defaultContext = $defaultContext; + } + + /** + * @param string $method + */ + public function setMethod($method) + { + $this->method = $method; + } + + public function write($level, $message, $context = []) + { + $message = iconv('UTF-8', 'UTF-8//IGNORE', $message); + $this->connection->notification($this->method, $this->defaultContext + [ + 'level' => $level, + 'timestamp' => microtime(true), + 'message' => $message, + 'context' => $context, + ]); + } +} diff --git a/vendor/gipfl/log/src/Writer/ProxyLogWriter.php b/vendor/gipfl/log/src/Writer/ProxyLogWriter.php new file mode 100644 index 0000000..78d5262 --- /dev/null +++ b/vendor/gipfl/log/src/Writer/ProxyLogWriter.php @@ -0,0 +1,14 @@ +<?php + +namespace gipfl\Log\Writer; + +use gipfl\Log\Logger; +use gipfl\Log\LogWriterWithContext; + +class ProxyLogWriter extends Logger implements LogWriterWithContext +{ + public function write($level, $message, $context = []) + { + $this->log($level, $message, $context); + } +} diff --git a/vendor/gipfl/log/src/Writer/SyslogWriter.php b/vendor/gipfl/log/src/Writer/SyslogWriter.php new file mode 100644 index 0000000..86b8254 --- /dev/null +++ b/vendor/gipfl/log/src/Writer/SyslogWriter.php @@ -0,0 +1,34 @@ +<?php + +namespace gipfl\Log\Writer; + +use gipfl\Log\LogLevel; +use gipfl\Log\LogWriter; +use function openlog; +use function syslog; + +class SyslogWriter implements LogWriter +{ + /** @var string */ + protected $ident; + + /** @var string */ + protected $facility; + + /** + * SyslogWriter constructor. + * @param string $ident + * @param string $facility + */ + public function __construct($ident, $facility) + { + $this->ident = $ident; + $this->facility = $facility; + } + + public function write($level, $message) + { + openlog($this->ident, LOG_PID, $this->facility); + syslog(LogLevel::mapNameToNumeric($level), str_replace("\n", ' ', $message)); + } +} diff --git a/vendor/gipfl/log/src/Writer/SystemdStdoutWriter.php b/vendor/gipfl/log/src/Writer/SystemdStdoutWriter.php new file mode 100644 index 0000000..bb66525 --- /dev/null +++ b/vendor/gipfl/log/src/Writer/SystemdStdoutWriter.php @@ -0,0 +1,61 @@ +<?php + +namespace gipfl\Log\Writer; + +use gipfl\Log\LogLevel; +use gipfl\Log\LogWriter; +use InvalidArgumentException; +use React\EventLoop\LoopInterface; +use React\Stream\WritableResourceStream; +use React\Stream\WritableStreamInterface; +use function is_int; +use function sprintf; + +class SystemdStdoutWriter implements LogWriter +{ + // local0 + const DEFAULT_FACILITY = 10; + + /** @var WritableStreamInterface */ + protected $stdOut; + + /** @var int */ + protected $facility = self::DEFAULT_FACILITY; + + /** + * SystemdStdoutWriter constructor. + * @param LoopInterface $loop + * @param WritableStreamInterface|null $stdOut + */ + public function __construct(LoopInterface $loop, WritableStreamInterface $stdOut = null) + { + if ($stdOut === null) { + $this->stdOut = new WritableResourceStream(STDOUT, $loop); + } else { + $this->stdOut = $stdOut; + } + } + + /** + * @param int $facility + */ + public function setFacility($facility) + { + if (! is_int($facility)) { + throw new InvalidArgumentException('Facility needs to be an integer'); + } + if ($facility < 0 || $facility > 23) { + throw new InvalidArgumentException("Facility needs to be between 0 and 23, got $facility"); + } + $this->facility = $facility; + } + + public function write($level, $message) + { + $this->stdOut->write(sprintf( + "<%d>%s\n", + LogLevel::mapNameToNumeric($level), + $message + )); + } +} diff --git a/vendor/gipfl/log/src/Writer/WritableStreamWriter.php b/vendor/gipfl/log/src/Writer/WritableStreamWriter.php new file mode 100644 index 0000000..4ae877f --- /dev/null +++ b/vendor/gipfl/log/src/Writer/WritableStreamWriter.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\Log\Writer; + +use gipfl\Log\LogWriter; +use React\Stream\WritableStreamInterface; + +class WritableStreamWriter implements LogWriter +{ + const DEFAULT_SEPARATOR = PHP_EOL; + + /** @var WritableStreamInterface */ + protected $stream; + + /** @var string */ + protected $separator = self::DEFAULT_SEPARATOR; + + /** + * WritableStreamWriter constructor. + * @param WritableStreamInterface $stream + */ + public function __construct(WritableStreamInterface $stream) + { + $this->setStream($stream); + } + + /** + * @param string $separator + */ + public function setSeparator($separator) + { + $this->separator = $separator; + } + + public function setStream(WritableStreamInterface $stream) + { + $this->stream = $stream; + } + + public function write($level, $message) + { + $this->stream->write("$level: $message" . $this->separator); + } +} diff --git a/vendor/gipfl/openrpc/composer.json b/vendor/gipfl/openrpc/composer.json new file mode 100644 index 0000000..dca0e6b --- /dev/null +++ b/vendor/gipfl/openrpc/composer.json @@ -0,0 +1,24 @@ +{ + "name": "gipfl/openrpc", + "description": "OpenRPC Connection implementation", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\OpenRpc\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "ext-json": "*" + } +} diff --git a/vendor/gipfl/openrpc/src/Components.php b/vendor/gipfl/openrpc/src/Components.php new file mode 100644 index 0000000..621a9a0 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Components.php @@ -0,0 +1,68 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * Holds a set of reusable objects for different aspects of the OpenRPC. All + * objects defined within the components object will have no effect on the API + * unless they are explicitly referenced from properties outside the components + * object. + * + * All the fixed fields declared are objects that MUST use keys that match the + * regular expression: ^[a-zA-Z0-9\.\-_]+$ + */ +class Components implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * An object to hold reusable Content Descriptor Objects + * + * @var ContentDescriptor[] Map[string, Content Descriptor Object] + */ + public $contentDescriptors; + + /** + * An object to hold reusable Schema Objects + * + * @var SchemaObject[] Map[string, Schema Object] + */ + public $schemas; + + /** + * An object to hold reusable Example Objects + * + * @var Example[] Map[string, Example Object] + */ + public $examples; + + /** + * An object to hold reusable Link Objects + * + * @var Link[] Map[string, Link Object] + */ + public $links; + + /** + * An object to hold reusable Error Objects + * + * @var Error[] Map[string, Error Object] + */ + public $errors; + + /** + * An object to hold reusable Example Pairing Objects + * + * @var ExamplePairing[] Map[string, Example Pairing Object] + */ + public $examplePairingObjects; + + /** + * An object to hold reusable Tag Objects + * + * @var TagObject[] Map[string, Tag Object] + */ + public $tags; +} diff --git a/vendor/gipfl/openrpc/src/Contact.php b/vendor/gipfl/openrpc/src/Contact.php new file mode 100644 index 0000000..e957719 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Contact.php @@ -0,0 +1,49 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * Contact information for the exposed API + */ +class Contact implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * The identifying name of the contact person/organization + * + * @var string|null + */ + public $name; + + /** + * The URL pointing to the contact information. MUST be in the format of a + * URL. + * + * @var string|null + */ + public $url; + + /** + * The email address of the contact person/organization. MUST be in the + * format of an email address. + * + * @var string|null + */ + public $email; + + /** + * Contact constructor. + * @param string|null $name + * @param string|null $url + * @param string|null $email + */ + public function __construct($name = null, $url = null, $email = null) + { + $this->name = $name; + $this->url = $url; + $this->email = $email; + } +} diff --git a/vendor/gipfl/openrpc/src/ContentDescriptor.php b/vendor/gipfl/openrpc/src/ContentDescriptor.php new file mode 100644 index 0000000..42772fb --- /dev/null +++ b/vendor/gipfl/openrpc/src/ContentDescriptor.php @@ -0,0 +1,73 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * Content Descriptors are objects that do just as they suggest - describe + * content. They are reusable ways of describing either parameters or result. + * They MUST have a schema. + */ +class ContentDescriptor implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. Name of the content that is being described. If the content + * described is a method parameter assignable by-name, this field SHALL + * define the parameter’s key (ie name). + * + * @var string + */ + public $name; + + /** + * A short summary of the content that is being described. + * + * @var string|null + */ + public $summary; + + /** + * A verbose explanation of the content descriptor behavior. GitHub Flavored + * Markdown syntax MAY be used for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * Determines if the content is a required field. Default value is false. + * + * @var boolean|null + */ + public $required; + + /** + * REQUIRED. Schema that describes the content. + * + * The Schema Object allows the definition of input and output data types. + * The Schema Objects MUST follow the specifications outline in the JSON + * Schema Specification 7 Alternatively, any time a Schema Object can be + * used, a Reference Object can be used in its place. This allows referencing + * definitions instead of defining them inline. + * + * @var SchemaObject + */ + public $schema; + + /** + * Specifies that the content is deprecated and SHOULD be transitioned out + * of usage. Default value is false. + * + * @var boolean|null + */ + public $deprecated; + + public function __construct($name, $schema) + { + $this->name = $name; + $this->schema = $schema; + } +} diff --git a/vendor/gipfl/openrpc/src/Error.php b/vendor/gipfl/openrpc/src/Error.php new file mode 100644 index 0000000..25ed818 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Error.php @@ -0,0 +1,54 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * Defines an application level error. + */ +class Error implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * Application Defined Error Code + * + * REQUIRED. A Number that indicates the error type that occurred. This + * MUST be an integer. The error codes from and including -32768 to -32000 + * are reserved for pre-defined errors. These pre-defined errors SHOULD be + * assumed to be returned from any JSON-RPC api. + * + * @var int + */ + public $code; + + /** + * REQUIRED. A String providing a short description of the error. The + * message SHOULD be limited to a concise single sentence. + * + * @var string + */ + public $message; + + /** + * A Primitive or Structured value that contains additional information + * about the error. This may be omitted. The value of this member is defined + * by the Server (e.g. detailed error information, nested errors etc.). + * + * @var mixed + */ + public $data; + + /** + * @param int $code + * @param string $message + * @param mixed|null $data + */ + public function __construct($code, $message, $data = null) + { + $this->code = $code; + $this->message = $message; + $this->data = $data; + } +} diff --git a/vendor/gipfl/openrpc/src/Example.php b/vendor/gipfl/openrpc/src/Example.php new file mode 100644 index 0000000..1e5aefe --- /dev/null +++ b/vendor/gipfl/openrpc/src/Example.php @@ -0,0 +1,36 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * The Example object is an object the defines an example that is intended to + * match a given Content Descriptor Schema. If the Content Descriptor Schema + * includes examples, the value from this Example Object supersedes the value + * of the schema example. + * + * In all cases, the example vaJsonSerializablelue is expected to be compatible with the type + * schema of its associated value. Tooling implementations MAY choose to + * validate compatibility automatically, and reject the example value(s) if + * incompatible. + */ +class Example implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** @var string|null Name for the example pairing */ + public $name; + + /** @var string|null A verbose explanation of the example pairing */ + public $summary; + + /** @var string|null Short description for the example pairing */ + public $description; + + /** @var <Example|Reference>[] Example parameters */ + public $params; + + /** @var Example|Reference Example result */ + public $result; +} diff --git a/vendor/gipfl/openrpc/src/ExamplePairing.php b/vendor/gipfl/openrpc/src/ExamplePairing.php new file mode 100644 index 0000000..6f34137 --- /dev/null +++ b/vendor/gipfl/openrpc/src/ExamplePairing.php @@ -0,0 +1,30 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * The example Pairing object consists of a set of example params and result. + * The result is what you can expect from the JSON-RPC service given the exact + * params. + */ +class ExamplePairing implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** @var string|null Name for the example pairing */ + public $name; + + /** @var string|null A verbose explanation of the example pairing */ + public $summary; + + /** @var string|null Short description for the example pairing */ + public $description; + + /** @var <Example|Reference>[] Example parameters */ + public $params; + + /** @var Example|Reference Example result */ + public $result; +} diff --git a/vendor/gipfl/openrpc/src/ExternalDocumentation.php b/vendor/gipfl/openrpc/src/ExternalDocumentation.php new file mode 100644 index 0000000..54b5883 --- /dev/null +++ b/vendor/gipfl/openrpc/src/ExternalDocumentation.php @@ -0,0 +1,37 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * Allows referencing an external resource for extended documentation + */ +class ExternalDocumentation implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. The URL for the target documentation. Value MUST be in the + * format of a URL. + * + * @var string + */ + public $url; + + /** + * A verbose explanation of the target documentation. GitHub Flavored Markdown + * syntax MAY be used for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * @param $url + */ + public function __construct($url) + { + $this->url = $url; + } +} diff --git a/vendor/gipfl/openrpc/src/Info.php b/vendor/gipfl/openrpc/src/Info.php new file mode 100644 index 0000000..3c957c9 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Info.php @@ -0,0 +1,69 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * The object provides metadata about the API. The metadata MAY be used by the + * clients if needed, and MAY be presented in editing or documentation + * generation tools for convenience. + */ +class Info implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. The title of the application + * + * @var string + */ + public $title; + /** + * A verbose description of the application. GitHub Flavored Markdown syntax + * MAY be used for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * A URL to the Terms of Service for the API. MUST be in the format of a URL + * + * @var string|null + */ + public $termsOfService; + + /** + * The contact information for the exposed API + * + * @var Contact|null + */ + public $contact; + + /** + * The license information for the exposed API + * + * @var License|null + */ + public $license; + + /** + * REQUIRED. The version of the OpenRPC document (which is distinct from the + * OpenRPC Specification version or the API implementation version) + * + * @var string + */ + public $version; + + /** + * Info constructor. + * @param string $title + * @param string $version + */ + public function __construct($title, $version) + { + $this->title = $title; + $this->version = $version; + } +} diff --git a/vendor/gipfl/openrpc/src/License.php b/vendor/gipfl/openrpc/src/License.php new file mode 100644 index 0000000..3bb8904 --- /dev/null +++ b/vendor/gipfl/openrpc/src/License.php @@ -0,0 +1,37 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * License information for the exposed API. + */ +class License implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. The license name used for the API. + * + * @var string + */ + public $name; + + /** + * A URL to the license used for the API. MUST be in the format of a URL. + * + * @var string|null + */ + public $url; + + /** + * @param string $name + * @param string|null $url + */ + public function __construct($name, $url = null) + { + $this->name = $name; + $this->url = $url; + } +} diff --git a/vendor/gipfl/openrpc/src/Link.php b/vendor/gipfl/openrpc/src/Link.php new file mode 100644 index 0000000..d0ac517 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Link.php @@ -0,0 +1,91 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * The Link object represents a possible design-time link for a result. The + * presence of a link does not guarantee the caller’s ability to successfully + * invoke it, rather it provides a known relationship and traversal mechanism + * between results and other methods. + * + * Unlike dynamic links (i.e. links provided in the result payload), the OpenRPC + * linking mechanism does not require link information in the runtime result. + * + * For computing links, and providing instructions to execute them, a runtime + * expression is used for accessing values in an method and using them as + * parameters while invoking the linked method. + */ +class Link implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. Canonical name of the link. + * + * @var string + */ + public $name; + + /** + * Short description for the link. + * + * @var string|null + */ + public $summary; + + /** + * A description of the link. GitHub Flavored Markdown syntax MAY be used + * for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * The name of an existing, resolvable OpenRPC method, as defined with a + * unique method. This field MUST resolve to a unique Method Object. As + * opposed to Open Api, Relative method values ARE NOT permitted. + * + * @var string|null + */ + public $method; + + /** + * A map representing parameters to pass to a method as specified with + * method. The key is the parameter name to be used, whereas the value can + * be a constant or a runtime expression to be evaluated and passed to the + * linked method. + * + * A linked method must be identified directly, and must exist in the list + * of methods defined by the Methods Object. + * + * When a runtime expression fails to evaluate, no parameter value is passed + * to the target method. + * + * Values from the result can be used to drive a linked method. + * + * Clients follow all links at their discretion. Neither permissions, nor + * the capability to make a successful call to that link, is guaranteed + * solely by the existence of a relationship. + * + * @var array Map[string, Any | RuntimeExpression] + */ + public $params; + + /** + * A server object to be used by the target method. + * + * @var Server|null + */ + public $server; + + /** + * @param string $name + */ + public function __construct($name) + { + $this->name = $name; + } +} diff --git a/vendor/gipfl/openrpc/src/Method.php b/vendor/gipfl/openrpc/src/Method.php new file mode 100644 index 0000000..54226e3 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Method.php @@ -0,0 +1,133 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * Describes the interface for the given method name. The method name is used + * as the method field of the JSON-RPC body. It therefore MUST be unique. + */ +class Method implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. The cannonical name for the method. The name MUST be unique + * within the methods array. + * + * @var string + */ + public $name; + + /** + * A list of tags for API documentation control. Tags can be used for + * logical grouping of methods by resources or any other qualifier. + * + * @var TagObject[]|Reference[] + */ + public $tags; + + /** + * A short summary of what the method does + * + * @var string|null + */ + public $summary; + + /** + * A verbose explanation of the method behavior. GitHub Flavored Markdown + * syntax MAY be used for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * Additional external documentation for this method + * + * @var ExternalDocumentation + */ + public $externalDocs; + + /** + * REQUIRED. A list of parameters that are applicable for this method. The + * list MUST NOT include duplicated parameters and therefore require name + * to be unique. The list can use the Reference Object to link to parameters + * that are defined by the Content Descriptor Object. All optional params + * (content descriptor objects with “required”: false) MUST be positioned + * after all required params in the list. + * + * @var <ContentDescriptor|Reference>[] + */ + public $params; + + /** + * REQUIRED. The description of the result returned by the method. It MUST + * be a Content Descriptor. + * + * @var ContentDescriptor|Reference + */ + public $result; + + /** + * Declares this method to be deprecated. Consumers SHOULD refrain from + * usage of the declared method. Default value is false. + * + * @var boolean + */ + public $deprecated; + + /** + * An alternative servers array to service this method. If an alternative + * servers array is specified at the Root level, it will be overridden by + * this value. + * + * @var Server[] + */ + public $servers; + + /** + * A list of custom application defined errors that MAY be returned. The + * Errors MUST have unique error codes. + * + * @var <Error|Reference>[] + */ + public $errors; + + /** + * A list of possible links from this method call + * + * @var <Link|Reference>[] + */ + public $links; + + /** + * The expected format of the parameters. As per the JSON-RPC 2.0 specification, + * the params of a JSON-RPC request object may be an array, object, or either + * (represented as by-position, by-name, and either respectively). When a method + * has a paramStructure value of by-name, callers of the method MUST send a + * JSON-RPC request object whose params field is an object. Further, the key + * names of the params object MUST be the same as the contentDescriptor.names + * for the given method. Defaults to "either". + * + * @var string "by-name" | "by-position" | "either" + */ + public $paramStructure; + + /** + * Array of Example Pairing Object where each example includes a valid + * params-to-result Content Descriptor pairing. + * + * @var ExamplePairing [] + */ + public $examples; + + /** + * @param $name + */ + public function __construct($name) + { + $this->name = $name; + } +} diff --git a/vendor/gipfl/openrpc/src/OpenRpcDocument.php b/vendor/gipfl/openrpc/src/OpenRpcDocument.php new file mode 100644 index 0000000..62be245 --- /dev/null +++ b/vendor/gipfl/openrpc/src/OpenRpcDocument.php @@ -0,0 +1,75 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * This is the root object of the OpenRPC document. The contents of this object + * represent a whole OpenRPC document. How this object is constructed or stored + * is outside the scope of the OpenRPC Specification. + */ +class OpenRpcDocument implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. This string MUST be the semantic version number of the OpenRPC + * Specification version that the OpenRPC document uses. The openrpc field + * SHOULD be used by tooling specifications and clients to interpret the + * OpenRPC document. This is not related to the API info.version string. + * + * @var string + */ + public $openrpc; + + /** + * REQUIRED. Provides metadata about the API. The metadata MAY be used by + * tooling as required. + * + * @var Info + */ + public $info; + + /** + * An array of Server Objects, which provide connectivity information to a + * target server. If the servers property is not provided, or is an empty + * array, the default value would be a Server Object with a url value of + * localhost. + * + * @var Server[]|null + */ + public $servers; + + /** + * REQUIRED. The available methods for the API. While it is required, the + * array may be empty (to handle security filtering, for example). + * + * @var Method[]|Reference[] + */ + public $methods = []; + + /** + * An element to hold various schemas for the specification + * + * @var Components|null + */ + public $components; + + /** + * Additional external documentation + * + * @var ExternalDocumentation|null + */ + public $externalDocs; + + /** + * @param string $openRpcVersion + * @param Info $info + */ + public function __construct($openRpcVersion, Info $info) + { + $this->openrpc = $openRpcVersion; + $this->info = $info; + } +} diff --git a/vendor/gipfl/openrpc/src/Reference.php b/vendor/gipfl/openrpc/src/Reference.php new file mode 100644 index 0000000..e954293 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reference.php @@ -0,0 +1,32 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * A simple object to allow referencing other components in the specification, + * internally and externally. + * + * The Reference Object is defined by JSON Schema and follows the same structure, + * behavior and rules. + */ +class Reference implements JsonSerializable +{ + /** @var string REQUIRED. The reference string */ + public $ref; + + /** + * @param string $ref + */ + public function __construct($ref) + { + $this->ref = $ref; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return (object) ['$ref' => $this->ref]; + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/MetaDataClass.php b/vendor/gipfl/openrpc/src/Reflection/MetaDataClass.php new file mode 100644 index 0000000..4e9427e --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/MetaDataClass.php @@ -0,0 +1,76 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +use InvalidArgumentException; +use ReflectionClass; +use ReflectionException; +use function lcfirst; +use function preg_match; + +class MetaDataClass +{ + /** @var MetaDataMethod[] */ + public $methods = []; + + /** @var string|null */ + public $error; + + /** + * @param string $class + * @throws ReflectionException + * @return static + */ + public static function analyze($class) + { + $info = new static(); + + $ref = new ReflectionClass($class); + + foreach ($ref->getMethods() as $method) { + $methodName = $method->getName(); + if (! preg_match('/^(.+)(Request|Notification)$/', $methodName, $match)) { + continue; + } + + $info->addMethod(MethodCommentParser::parseMethod( + $match[1], + lcfirst($match[2]), + $method->getDocComment() + )); + } + + return $info; + } + + public function addMethod(MetaDataMethod $method) + { + $name = $method->name; + if (isset($this->methods[$name])) { + throw new InvalidArgumentException("Cannot add method '$name' twice"); + } + + $this->methods[$name] = $method; + } + + /** + * @return MetaDataMethod[] + */ + public function getMethods() + { + return $this->methods; + } + + /** + * @param $name + * @return MetaDataMethod + */ + public function getMethod($name) + { + if (isset($this->methods[$name])) { + return $this->methods[$name]; + } + + throw new InvalidArgumentException("There is no '$name' method"); + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/MetaDataMethod.php b/vendor/gipfl/openrpc/src/Reflection/MetaDataMethod.php new file mode 100644 index 0000000..e711e6e --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/MetaDataMethod.php @@ -0,0 +1,111 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +use InvalidArgumentException; + +class MetaDataMethod +{ + /** @var string */ + public $name; + + /** @var string Either 'request' or 'notification' */ + public $requestType; + + /** @var string */ + public $resultType; + + /** @var MetaDataParameter[] */ + public $parameters = []; + + /** @var string */ + public $title; + + /** @var string */ + public $description; + + public function __construct($name, $requestType) + { + $this->name = $name; + $this->requestType = $requestType; + } + + public function addParsed(MethodCommentParser $parser) + { + $this->resultType = $parser->getResultType(); + $this->parameters = $parser->getParams(); + $this->title = $parser->getTitle(); + $this->description = $parser->getDescription(); + + return $this; + } + + /** + * @param MetaDataParameter $parameter + */ + public function addParameter(MetaDataParameter $parameter) + { + $this->parameters[$parameter->getName()] = $parameter; + } + + /** + * @param $name + * @return MetaDataParameter + */ + public function getParameter($name) + { + if (isset($this->parameters[$name])) { + return $this->parameters[$name]; + } + + throw new InvalidArgumentException("There is no '$name' parameter" . print_r($this->parameters, 1)); + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getRequestType() + { + return $this->requestType; + } + + /** + * @return string + */ + public function getResultType() + { + return $this->resultType ?: 'void'; + } + + /** + * @return MetaDataParameter[] + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/MetaDataParameter.php b/vendor/gipfl/openrpc/src/Reflection/MetaDataParameter.php new file mode 100644 index 0000000..7ed76dd --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/MetaDataParameter.php @@ -0,0 +1,46 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class MetaDataParameter +{ + /** @var string */ + public $name; + + /** @var string */ + public $type; + + /** @var string */ + public $description; + + public function __construct($name, $type, $description = null) + { + $this->name = $name; + $this->type = $type; + $this->description = $description; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/MetaDataTagParser.php b/vendor/gipfl/openrpc/src/Reflection/MetaDataTagParser.php new file mode 100644 index 0000000..d1df585 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/MetaDataTagParser.php @@ -0,0 +1,60 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class MetaDataTagParser +{ + const DEFAULT_TAG_TYPE = Tag::class; + + const SPECIAL_TAGS = [ + 'param' => ParamTag::class, + 'throws' => ThrowsTag::class, + 'return' => ReturnTag::class, + ]; + + protected $tagType; + + protected $string; + + public function __construct($tagType, $string) + { + $this->tagType = $tagType; + $this->string = $string; + } + + public function getTag() + { + $type = $this->getTagType(); + $tags = static::SPECIAL_TAGS; + if (isset($tags[$type])) { + $class = self::SPECIAL_TAGS[$type]; + } else { + $class = self::DEFAULT_TAG_TYPE; + } + + return new $class($type, $this->getString()); + } + + /** + * @return string + */ + public function getTagType() + { + return $this->tagType; + } + + /** + * @return string + */ + public function getString() + { + return $this->string; + } + + public function appendValueString($string) + { + $this->string .= $string; + + return $this; + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/MetaDataTagSet.php b/vendor/gipfl/openrpc/src/Reflection/MetaDataTagSet.php new file mode 100644 index 0000000..a246fcb --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/MetaDataTagSet.php @@ -0,0 +1,69 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class MetaDataTagSet +{ + /** @var Tag[] */ + protected $tags; + + public function __construct() + { + $this->tags = []; + } + + public function add(Tag $tag) + { + $this->tags[] = $tag; + } + + /** + * @param string $type + * @return static + */ + public function byType($type) + { + $set = new static(); + foreach ($this->tags as $tag) { + if ($tag->tagType === $type) { + $set->add($tag); + } + } + + return $set; + } + + /** + * @return MetaDataParameter[] + */ + public function getParams() + { + $result = []; + foreach ($this->byType('param')->getTags() as $tag) { + assert($tag instanceof ParamTag); + $result[] = new MetaDataParameter($tag->name, $tag->dataType, $tag->description); + // TODO: variadic! + } + + return $result; + } + + /** + * @return string|null + */ + public function getReturnType() + { + foreach ($this->byType('return')->getTags() as $tag) { + assert($tag instanceof ReturnTag); + // TODO: return a class, we need the description + return $tag->dataType; + } + + return null; + } + + public function getTags() + { + return $this->tags; + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/MethodCommentParser.php b/vendor/gipfl/openrpc/src/Reflection/MethodCommentParser.php new file mode 100644 index 0000000..6f20f93 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/MethodCommentParser.php @@ -0,0 +1,163 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +use function explode; +use function implode; +use function preg_match; +use function preg_replace; +use function substr; +use function trim; + +class MethodCommentParser +{ + const REGEXP_START_OF_COMMENT = '~^\s*/\*\*\n~s'; + const REGEXP_COMMENT_LINE_START = '~^\s*\*\s?~'; + const REGEXP_END_OF_COMMENT = '~\n\s*\*/\s*~s'; + const REGEXP_TAG_TYPE_VALUE = '/^@([A-z0-9]+)\s+(.+?)$/'; + + protected $paragraphs = []; + protected $currentParagraph; + + /** @var MetaDataMethod */ + protected $meta; + + /** @var MetaDataTagParser|null */ + protected $currentTag; + + /** @var MetaDataTagSet */ + protected $tags; + + protected function __construct(MetaDataMethod $meta) + { + $this->meta = $meta; + $this->tags = new MetaDataTagSet(); + } + + public function getTitle() + { + return $this->meta->title; + } + + public function getParams() + { + return $this->getTags()->getParams(); + } + + public function getResultType() + { + return $this->getTags()->getReturnType(); + } + + public function getDescription() + { + return implode("\n", $this->paragraphs); + } + + public function getTags() + { + return $this->tags; + } + + protected function parseLine($line) + { + // Strip * at line start + $line = preg_replace(self::REGEXP_COMMENT_LINE_START, '', $line); + $line = trim($line); + if (preg_match(self::REGEXP_TAG_TYPE_VALUE, $line, $match)) { + $this->finishCurrentObjects(); + $this->currentTag = new MetaDataTagParser($match[1], $match[2]); + return; + } + + if ($this->currentTag) { + $this->currentTag->appendValueString($line); + return; + } + + $this->eventuallyFinishCurrentTag(); + $this->appendToParagraph($line); + } + + protected function appendToParagraph($line) + { + if (trim($line) === '') { + $this->eventuallyFinishCurrentLine(); + return; + } + + if ($this->currentParagraph === null) { + $this->currentParagraph = & $this->paragraphs[]; + $this->currentParagraph = $line; + } else { + if (substr($line, 0, 2) === ' ') { + $this->currentParagraph .= "\n" . $line; + } else { + $this->currentParagraph .= ' ' . $line; + } + } + } + + protected function finishCurrentObjects() + { + $this->eventuallyFinishCurrentTag(); + $this->eventuallyFinishCurrentLine(); + } + + protected function eventuallyFinishCurrentTag() + { + if ($this->currentTag) { + $this->tags->add($this->currentTag->getTag()); + $this->currentTag = null; + } + } + + protected function eventuallyFinishCurrentLine() + { + if ($this->currentParagraph !== null) { + unset($this->currentParagraph); + $this->currentParagraph = null; + } + } + + protected function parse($plain) + { + foreach (explode("\n", $plain) as $line) { + $this->parseLine($line); + } + $this->finishCurrentObjects(); + } + + public static function parseMethod($methodName, $methodType, $raw) + { + $meta = new MetaDataMethod($methodName, $methodType); + $self = new static($meta); + $plain = (string) $raw; + static::stripStartOfComment($plain); + static::stripEndOfComment($plain); + $self->parse($plain); + $meta->addParsed($self); + + return $meta; + } + + /** + * Removes comment start -> /** + * + * @param $string + */ + protected static function stripStartOfComment(&$string) + { + $string = preg_replace(self::REGEXP_START_OF_COMMENT, '', $string); + } + + /** + * Removes comment end -> * / + * + * @param $string + */ + protected static function stripEndOfComment(&$string) + { + $string = preg_replace(self::REGEXP_END_OF_COMMENT, "\n", $string); + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/ParamTag.php b/vendor/gipfl/openrpc/src/Reflection/ParamTag.php new file mode 100644 index 0000000..32a8b33 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/ParamTag.php @@ -0,0 +1,41 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class ParamTag extends Tag +{ + public $name; + + public $dataType; + + public $description; + + public $isVariadic = false; + + protected function parseTagValue($value) + { + $parts = preg_split('/(\s+)/us', $value, 3, PREG_SPLIT_DELIM_CAPTURE); + if (substr($parts[0], 0, 1) !== '$' && substr($parts[0], 0, 4) !== '...$') { + $this->dataType = array_shift($parts); + array_shift($parts); + } + if (empty($parts)) { + return; + } + + if (substr($parts[0], 0, 1) === '$') { + $this->name = substr($parts[0], 1); + array_shift($parts); + array_shift($parts); + } elseif (substr($parts[0], 0, 4) !== '...$') { + $this->name = substr($parts[0], 4); + $this->isVariadic = true; + array_shift($parts); + array_shift($parts); + } + + if (! empty($parts)) { + $this->description = implode($parts); + } + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/ReturnTag.php b/vendor/gipfl/openrpc/src/Reflection/ReturnTag.php new file mode 100644 index 0000000..0a147cf --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/ReturnTag.php @@ -0,0 +1,7 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class ReturnTag extends TypeDescriptionTag +{ +} diff --git a/vendor/gipfl/openrpc/src/Reflection/Tag.php b/vendor/gipfl/openrpc/src/Reflection/Tag.php new file mode 100644 index 0000000..038cb53 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/Tag.php @@ -0,0 +1,37 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class Tag +{ + /** @var string */ + public $tagType; + + /** @var string */ + public $tagValue; + + public function __construct($tagType, $tagValue) + { + $this->tagType = $tagType; + $this->setTagValue($tagValue); + $this->parseTagValue(trim($tagValue)); + } + + public function setTagValue($value) + { + $this->tagValue = $value; + + return $this; + } + + /** + * Parse Tag value into Tag-specific properties + * + * Override this method for specific tag types + * + * @param $tagValue + */ + protected function parseTagValue($tagValue) + { + } +} diff --git a/vendor/gipfl/openrpc/src/Reflection/ThrowsTag.php b/vendor/gipfl/openrpc/src/Reflection/ThrowsTag.php new file mode 100644 index 0000000..42d1fd6 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/ThrowsTag.php @@ -0,0 +1,7 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class ThrowsTag extends TypeDescriptionTag +{ +} diff --git a/vendor/gipfl/openrpc/src/Reflection/TypeDescriptionTag.php b/vendor/gipfl/openrpc/src/Reflection/TypeDescriptionTag.php new file mode 100644 index 0000000..fd090b2 --- /dev/null +++ b/vendor/gipfl/openrpc/src/Reflection/TypeDescriptionTag.php @@ -0,0 +1,27 @@ +<?php + +namespace gipfl\OpenRpc\Reflection; + +class TypeDescriptionTag extends Tag +{ + public $dataType; + + public $description; + + protected function parseTagValue($value) + { + if (empty($value)) { + return; + } + $parts = preg_split('/(\s+)/us', trim($value), 2, PREG_SPLIT_DELIM_CAPTURE); + + $this->dataType = array_shift($parts); + array_shift($parts); + + if (empty($parts)) { + return; + } + + $this->description = implode($parts); + } +} diff --git a/vendor/gipfl/openrpc/src/Server.php b/vendor/gipfl/openrpc/src/Server.php new file mode 100644 index 0000000..0d03c9f --- /dev/null +++ b/vendor/gipfl/openrpc/src/Server.php @@ -0,0 +1,74 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * An object representing a Server. + */ +class Server implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. A name to be used as the canonical name for the server. + * + * @var string + */ + public $name; + + /** + * REQUIRED. A URL to the target host. This URL supports Server Variables and + * MAY be relative, to indicate that the host location is relative to the + * location where the OpenRPC document is being served. Server Variables are + * passed into the Runtime Expression to produce a server URL. + * + * Runtime expressions allow the user to define an expression which will + * evaluate to a string once the desired value(s) are known. They are used + * when the desired value of a link or server can only be constructed at + * run time. This mechanism is used by Link Objects and Server Variables. + * + * The runtime expression makes use of JSON Template Language syntax: + * + * https://tools.ietf.org/html/draft-jonas-json-template-language-01 + * + * Runtime expressions preserve the type of the referenced value. + * + * @var string RuntimeExpression + */ + public $url; + + /** + * A short summary of what the server is. + * + * @var string|null + */ + public $summary; + + /** + * An optional string describing the host designated by the URL. GitHub + * Flavored Markdown syntax MAY be used for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * A map between a variable name and its value. The value is passed into + * the Runtime Expression to produce a server URL. + * + * @var ServerVariable Map[string, Server Variable Object] + */ + public $variables; + + /** + * @param string $name + * @param string $url + */ + public function __construct($name, $url) + { + $this->name = $name; + $this->url = $url; + } +} diff --git a/vendor/gipfl/openrpc/src/ServerVariable.php b/vendor/gipfl/openrpc/src/ServerVariable.php new file mode 100644 index 0000000..047dcf6 --- /dev/null +++ b/vendor/gipfl/openrpc/src/ServerVariable.php @@ -0,0 +1,46 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * An object representing a Server Variable for server URL template substitution. + */ +class ServerVariable implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. The default value to use for substitution, which SHALL be sent + * if an alternate value is not supplied. Note this behavior is different + * than the Schema Object’s treatment of default values, because in those + * cases parameter values are optional. + * + * @var string + */ + public $default; + + /** + * An optional description for the server variable. GitHub Flavored Markdown + * syntax MAY be used for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * An enumeration of string values to be used if the substitution options are from a limited set. + * + * @var string[] + */ + public $enum; + + /** + * @param string $default + */ + public function __construct($default) + { + $this->default = $default; + } +} diff --git a/vendor/gipfl/openrpc/src/SimpleJsonSerializer.php b/vendor/gipfl/openrpc/src/SimpleJsonSerializer.php new file mode 100644 index 0000000..0427c7b --- /dev/null +++ b/vendor/gipfl/openrpc/src/SimpleJsonSerializer.php @@ -0,0 +1,14 @@ +<?php + +namespace gipfl\OpenRpc; + +trait SimpleJsonSerializer +{ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return (object) array_filter(get_object_vars($this), function ($value) { + return $value !== null; + }); + } +} diff --git a/vendor/gipfl/openrpc/src/TagObject.php b/vendor/gipfl/openrpc/src/TagObject.php new file mode 100644 index 0000000..2d69d1d --- /dev/null +++ b/vendor/gipfl/openrpc/src/TagObject.php @@ -0,0 +1,48 @@ +<?php + +namespace gipfl\OpenRpc; + +use JsonSerializable; + +/** + * Adds metadata to a single tag that is used by the Method Object. It is not + * mandatory to have a Tag Object per tag defined in the Method Object instances + */ +class TagObject implements JsonSerializable +{ + use SimpleJsonSerializer; + + /** + * REQUIRED. The name of the tag. + * + * @var string + */ + public $name; + + /** + * A short summary of the tag. + * + * @var string|null + */ + public $summary; + + /** + * A verbose explanation for the tag. GitHub Flavored Markdown syntax MAY + * be used for rich text representation. + * + * @var string|null + */ + public $description; + + /** + * Additional external documentation for this tag. + * + * @var ExternalDocumentation + */ + public $externalDocs; + + public function __construct($name) + { + $this->name = $name; + } +} diff --git a/vendor/gipfl/process/composer.json b/vendor/gipfl/process/composer.json new file mode 100644 index 0000000..21e9971 --- /dev/null +++ b/vendor/gipfl/process/composer.json @@ -0,0 +1,25 @@ +{ + "name": "gipfl/process", + "description": "Process-related utility classes", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "gipfl\\Process\\": "src/" + } + }, + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "require": { + "php": ">=5.6.0", + "ext-json": "*", + "react/child-process": ">=0.6", + "react/promise": "^2", + "gipfl/json": ">=0.1", + "gipfl/linux-health": ">=0.2" + } +} diff --git a/vendor/gipfl/process/src/FinishedProcessState.php b/vendor/gipfl/process/src/FinishedProcessState.php new file mode 100644 index 0000000..fd70292 --- /dev/null +++ b/vendor/gipfl/process/src/FinishedProcessState.php @@ -0,0 +1,66 @@ +<?php + +namespace gipfl\Process; + +class FinishedProcessState +{ + /** @var int|null */ + protected $exitCode; + + /** @var int|null */ + protected $termSignal; + + public function __construct($exitCode, $termSignal) + { + $this->exitCode = $exitCode; + $this->termSignal = $termSignal; + } + + public function succeeded() + { + return $this->exitCode === 0; + } + + /** + * @return int|null + */ + public function getExitCode() + { + return $this->exitCode; + } + + public function getTermSignal() + { + return $this->termSignal; + } + + public function getCombinedExitCode() + { + if ($this->exitCode === null) { + if ($this->termSignal === null) { + return 255; + } else { + return 255 + $this->termSignal; + } + } else { + return $this->exitCode; + } + } + + public function getReason() + { + if ($this->exitCode === null) { + if ($this->termSignal === null) { + return 'Process died'; + } else { + return 'Process got terminated with SIGNAL ' . $this->termSignal; + } + } else { + if ($this->exitCode === 0) { + return 'Process finished successfully'; + } else { + return 'Process exited with exit code ' . $this->exitCode; + } + } + } +} diff --git a/vendor/gipfl/process/src/ProcessInfo.php b/vendor/gipfl/process/src/ProcessInfo.php new file mode 100644 index 0000000..db740d1 --- /dev/null +++ b/vendor/gipfl/process/src/ProcessInfo.php @@ -0,0 +1,89 @@ +<?php + +namespace gipfl\Process; + +use gipfl\Json\JsonSerialization; +use gipfl\LinuxHealth\Memory; +use React\ChildProcess\Process; + +class ProcessInfo implements JsonSerialization +{ + /** @var ?int */ + protected $pid; + + /** @var string */ + protected $command; + + /** @var bool */ + protected $running; + + /** @var ?object */ + protected $memory; + + public static function forProcess(Process $process) + { + $self = new static(); + $self->pid = $process->getPid(); + $self->command = $process->getCommand(); + $self->running = $process->isRunning(); + if ($memory = Memory::getUsageForPid($self->pid)) { + $self->memory = $memory; + } + + return $self; + } + + public static function fromSerialization($any) + { + $self = new static(); + $self->pid = $any->pid; + $self->command = $any->command; + $self->running = $any->running; + $self->memory = $any->memory; + + return $self; + } + + /** + * @return int|null + */ + public function getPid() + { + return $this->pid; + } + + /** + * @return string + */ + public function getCommand() + { + return $this->command; + } + + /** + * @return bool + */ + public function isRunning() + { + return $this->running; + } + + /** + * @return object|null + */ + public function getMemory() + { + return $this->memory; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return (object) [ + 'pid' => $this->pid, + 'command' => $this->command, + 'running' => $this->running, + 'memory' => $this->memory, + ]; + } +} diff --git a/vendor/gipfl/process/src/ProcessKiller.php b/vendor/gipfl/process/src/ProcessKiller.php new file mode 100644 index 0000000..cf904ce --- /dev/null +++ b/vendor/gipfl/process/src/ProcessKiller.php @@ -0,0 +1,81 @@ +<?php + +namespace gipfl\Process; + +use React\ChildProcess\Process as ChildProcess; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use function React\Promise\resolve; + +class ProcessKiller +{ + /** + * @param ChildProcess $process + * @param LoopInterface $loop + * @param int $timeout + * @return \React\Promise\ExtendedPromiseInterface + */ + public static function terminateProcess(ChildProcess $process, LoopInterface $loop, $timeout = 0) + { + $processes = new ProcessList(); + $processes->attach($process); + return static::terminateProcesses($processes, $loop, $timeout); + } + + /** + * @param ProcessList $processes + * @param LoopInterface $loop + * @param int $timeout + * @return \React\Promise\ExtendedPromiseInterface + */ + public static function terminateProcesses(ProcessList $processes, LoopInterface $loop, $timeout = 5) + { + if ($processes->count() === 0) { + return resolve(); + } + $deferred = new Deferred(); + $killTimer = $loop->addTimer($timeout, function () use ($deferred, $processes, $loop) { + /** @var ChildProcess $process */ + foreach ($processes as $process) { + $pid = $process->getPid(); + // Logger::error("Process $pid is still running, sending SIGKILL"); + $process->terminate(SIGKILL); + } + + // Let's add a bit of a delay after KILLing + $loop->addTimer(0.1, function () use ($deferred) { + $deferred->resolve(); + }); + }); + + $timer = $loop->addPeriodicTimer($timeout / 20, function () use ( + $deferred, + $processes, + $loop, + &$timer, + $killTimer + ) { + $stopped = []; + /** @var ChildProcess $process */ + foreach ($processes as $process) { + if (! $process->isRunning()) { + $stopped[] = $process; + } + } + foreach ($stopped as $process) { + $processes->detach($process); + } + if ($processes->count() === 0) { + $loop->cancelTimer($timer); + $loop->cancelTimer($killTimer); + $deferred->resolve(); + } + }); + /** @var ChildProcess $process */ + foreach ($processes as $process) { + $process->terminate(SIGTERM); + } + + return $deferred->promise(); + } +} diff --git a/vendor/gipfl/process/src/ProcessList.php b/vendor/gipfl/process/src/ProcessList.php new file mode 100644 index 0000000..a3f7c30 --- /dev/null +++ b/vendor/gipfl/process/src/ProcessList.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\Process; + +/** + * @method ChildProcess current + */ +use Evenement\EventEmitterInterface; +use Evenement\EventEmitterTrait; +use InvalidArgumentException; +use React\ChildProcess\Process as ChildProcess; +use SplObjectStorage; + +class ProcessList extends SplObjectStorage implements EventEmitterInterface +{ + const ON_ATTACHED = 'attached'; + const ON_DETACHED = 'detached'; + + use EventEmitterTrait; + + #[\ReturnTypeWillChange] + public function attach($object, $info = null) + { + if (! $object instanceof ChildProcess || $info !== null) { + throw new InvalidArgumentException(sprintf( + 'Can attach only %s instances', + ChildProcess::class + )); + } + $object->on('exit', function () use ($object) { + $this->detach($object); + }); + + parent::attach($object, $info); + $this->emit(self::ON_ATTACHED, [$object]); + } + + #[\ReturnTypeWillChange] + public function detach($object) + { + parent::detach($object); + $this->emit(self::ON_DETACHED, [$object]); + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/LICENSE b/vendor/gipfl/protocol-jsonrpc/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/protocol-jsonrpc/composer.json b/vendor/gipfl/protocol-jsonrpc/composer.json new file mode 100644 index 0000000..4a202eb --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/composer.json @@ -0,0 +1,34 @@ +{ + "name": "gipfl/protocol-jsonrpc", + "description": "JsonRPC Connection implementation", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Protocol\\JsonRpc\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "ext-json": "*", + "gipfl/json": ">=0.1", + "gipfl/openrpc": "^0.2.1", + "gipfl/protocol": ">=0.2", + "psr/log": ">=1.1", + "react/promise": ">=2.7", + "react/stream": ">=1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^7.5 || ^6.5 || ^5.7", + "squizlabs/php_codesniffer": "^3.6" + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Connection.php b/vendor/gipfl/protocol-jsonrpc/src/Connection.php new file mode 100644 index 0000000..be4b33f --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Connection.php @@ -0,0 +1,310 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use Evenement\EventEmitterTrait; +use Exception; +use gipfl\Json\JsonEncodeException; +use InvalidArgumentException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use React\Promise\Deferred; +use React\Promise\Promise; +use React\Stream\DuplexStreamInterface; +use React\Stream\Util; +use RuntimeException; +use function call_user_func_array; +use function is_object; +use function mt_rand; +use function preg_quote; +use function preg_split; +use function React\Promise\reject; +use function sprintf; + +/** + * @deprecated Please use JsonRpcConection + */ +class Connection implements LoggerAwareInterface +{ + use EventEmitterTrait; + use LoggerAwareTrait; + + /** @var DuplexStreamInterface */ + protected $connection; + + /** @var array */ + protected $handlers = []; + + /** @var Deferred[] */ + protected $pending = []; + + protected $nsSeparator = '.'; + + protected $nsRegex = '/\./'; + + protected $unknownErrorCount = 0; + + public function handle(DuplexStreamInterface $connection) + { + $this->connection = $connection; + $this->connection->on('data', function ($data) { + try { + $this->handlePacket(Packet::decode($data)); + } catch (Exception $error) { + echo $error->getMessage() . "\n"; + $this->unknownErrorCount++; + if ($this->unknownErrorCount === 3) { + $this->close(); + } + $response = new Response(); + $response->setError(Error::forException($error)); + $this->connection->write($response->toString()); + } + }); + $connection->on('close', function () { + $this->rejectAllPendingRequests('Connection closed'); + }); + // TODO: figure out whether and how to deal with the pipe event + Util::forwardEvents($connection, $this, ['end', 'error', 'close', 'drain']); + } + + public function setNamespaceSeparator($separator) + { + $this->nsSeparator = $separator; + $this->nsRegex = '/' . preg_quote($separator, '/') . '/'; + + return $this; + } + + /** + * @param Packet $packet + */ + protected function handlePacket(Packet $packet) + { + if ($packet instanceof Response) { + $this->handleResponse($packet); + } elseif ($packet instanceof Request) { + $this->handleRequest($packet); + } elseif ($packet instanceof Notification) { + $this->handleNotification($packet); + } else { + // Will not happen as long as there is no bug in Packet + throw new RuntimeException('Packet was neither Request/Notification nor Response'); + } + } + + protected function handleResponse(Response $response) + { + $id = $response->getId(); + if (isset($this->pending[$id])) { + $promise = $this->pending[$id]; + unset($this->pending[$id]); + $promise->resolve($response); + } else { + $this->handleUnmatchedResponse($response); + } + } + + protected function handleUnmatchedResponse(Response $response) + { + // Ignore. Log? + } + + protected function handleRequest(Request $request) + { + $result = $this->handleNotification($request); + $this->sendResultForRequest($request, $result); + } + + protected function sendResultForRequest(Request $request, $result) + { + if ($result instanceof Error) { + $response = Response::forRequest($request); + $response->setError($result); + + $this->connection->write($response->toString()); + } elseif ($result instanceof Promise) { + $result->then(function ($result) use ($request) { + $this->sendResultForRequest($request, $result); + })->otherwise(function ($error) use ($request) { + $response = Response::forRequest($request); + if ($error instanceof Exception) { + $response->setError(Error::forException($error)); + } else { + $response->setError(new Error(Error::INTERNAL_ERROR, $error)); + } + // TODO: Double-check, this used to loop + $this->connection->write($response->toString()); + }); + } else { + $response = Response::forRequest($request); + $response->setResult($result); + $this->connection->write($response->toString()); + } + } + + /** + * @param Notification $notification + * @return Error|mixed + */ + protected function handleNotification(Notification $notification) + { + $method = $notification->getMethod(); + if (\strpos($method, $this->nsSeparator) === false) { + $namespace = null; + } else { + list($namespace, $method) = preg_split($this->nsRegex, $method, 2); + } + + try { + $response = $this->call($namespace, $method, $notification); + + return $response; + } catch (Exception $exception) { + return Error::forException($exception); + } + } + + /** + * @param Request $request + * @return \React\Promise\PromiseInterface + */ + public function sendRequest(Request $request) + { + $id = $request->getId(); + if ($id === null) { + $id = $this->getNextRequestId(); + $request->setId($id); + } + if (isset($this->pending[$id])) { + throw new InvalidArgumentException( + "A request with id '$id' is already pending" + ); + } + if (!$this->connection->isWritable()) { + return reject(new Exception('Cannot write to socket')); + } + try { + $this->connection->write($request->toString()); + } catch (JsonEncodeException $e) { + return reject($e->getMessage()); + } + $deferred = new Deferred(); + $this->pending[$id] = $deferred; + + return $deferred->promise()->then(function (Response $response) use ($deferred) { + if ($response->isError()) { + $deferred->reject(new RuntimeException($response->getError()->getMessage())); + } else { + $deferred->resolve($response->getResult()); + } + }, function (Exception $e) use ($deferred) { + $deferred->reject($e); + }); + } + + public function request($method, $params = null) + { + return $this->sendRequest(new Request($method, $this->getNextRequestId(), $params)); + } + + protected function getNextRequestId() + { + for ($i = 0; $i < 100; $i++) { + $id = mt_rand(1, 1000000000); + if (!isset($this->pending[$id])) { + return $id; + } + } + + throw new RuntimeException('Unable to generate a free random request ID, gave up after 100 attempts'); + } + + /** + * @param Request|mixed $request + */ + public function forgetRequest($request) + { + if ($request instanceof Request) { + unset($this->pending[$request->getId()]); + } else { + unset($this->pending[$request]); + } + } + + /** + * @param Notification $packet + */ + public function sendNotification(Notification $packet) + { + $this->connection->write($packet->toString()); + } + + /** + * @param string $method + * @param null $params + */ + public function notification($method, $params = null) + { + $notification = new Notification($method, $params); + $this->sendNotification($notification); + } + + /** + * @param $namespace + * @param $handler + * @return Connection + */ + public function setHandler($handler, $namespace = null) + { + $this->handlers[$namespace] = $handler; + + return $this; + } + + protected function call($namespace, $method, Notification $packet) + { + if (isset($this->handlers[$namespace])) { + $handler = $this->handlers[$namespace]; + if ($handler instanceof PacketHandler) { + return $handler->handle($packet); + } + + // Legacy handlers, deprecated: + $params = $packet->getParams(); + if (is_object($params)) { + return $handler->$method($params); + } + + return call_user_func_array([$handler, $method], $params); + } + + $error = new Error(Error::METHOD_NOT_FOUND); + $error->setMessage(sprintf( + '%s: %s%s%s', + $error->getMessage(), + $namespace, + $this->nsSeparator, + $method + )); + + return $error; + } + + protected function rejectAllPendingRequests($message) + { + foreach ($this->pending as $pending) { + $pending->reject(new Exception()); + } + $this->pending = []; + } + + public function close() + { + if ($this->connection) { + $this->connection->close(); + $this->handlers = []; + $this->connection = null; + } + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Error.php b/vendor/gipfl/protocol-jsonrpc/src/Error.php new file mode 100644 index 0000000..dc1d639 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Error.php @@ -0,0 +1,199 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use Exception; +use JsonSerializable; +use TypeError; + +class Error implements JsonSerializable +{ + const PARSE_ERROR = -32700; + + const INVALID_REQUEST = -32600; + + const METHOD_NOT_FOUND = -32601; + + const INVALID_PARAMS = 32602; + + const INTERNAL_ERROR = 32603; + + // Reserved for implementation-defined server-errors: + const MIN_CUSTOM_ERROR = -32000; + + const MAX_CUSTOM_ERROR = -32099; + + protected static $wellKnownErrorCodes = [ + self::PARSE_ERROR, + self::INVALID_REQUEST, + self::METHOD_NOT_FOUND, + self::INVALID_PARAMS, + self::INTERNAL_ERROR, + ]; + + protected static $errorMessages = [ + self::PARSE_ERROR => 'Invalid JSON was received by the server.', + self::INVALID_REQUEST => 'The JSON sent is not a valid Request object', + self::METHOD_NOT_FOUND => 'The method does not exist / is not available', + self::INVALID_PARAMS => 'Invalid method parameter(s)', + self::INTERNAL_ERROR => 'Internal JSON-RPC error', + ]; + + protected static $defaultCustomMessage = 'Server error. Reserved for implementation-defined server-errors.'; + + /** @var int */ + protected $code; + + /** @var string */ + protected $message; + + /** @var mixed|null */ + protected $data; + + /** + * Error constructor. + * @param int $code + * @param string $message + * @param mixed $data + */ + public function __construct($code, $message = null, $data = null) + { + if ($message === null) { + if ($this->isCustomErrorCode($code)) { + $message = self::$defaultCustomMessage; + } elseif (static::isWellKnownErrorCode($code)) { + $message = self::$errorMessages[$code]; + } else { + $message = 'Unknown error'; + } + } + $this->code = $code; + $this->message = $message; + $this->data = $data; + } + + public static function forException(Exception $exception) + { + $code = $exception->getCode(); + if (! static::isCustomErrorCode($code) + && ! static::isWellKnownErrorCode($code) + ) { + $code = self::INTERNAL_ERROR; + } + if (static::isWellKnownErrorCode($code) && $code !== self::INTERNAL_ERROR) { + $data = null; + } else { + $data = $exception->getTraceAsString(); + } + if (function_exists('iconv')) { + $data = iconv('UTF-8', 'UTF-8//IGNORE', $data); + } + + return new Error($code, sprintf( + '%s in %s(%d)', + $exception->getMessage(), + $exception->getFile(), + $exception->getLine() + ), $data); + } + + public static function forTypeError(TypeError $error) + { + $code = self::INVALID_PARAMS; + + return new Error($code, sprintf( + '%s in %s(%d)', + $error->getMessage(), + $error->getFile(), + $error->getLine() + )); + } + + /** + * @return int + */ + public function getCode() + { + return $this->code; + } + + /** + * @param int $code + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + return $this; + } + + /** + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * @param string $message + * @return $this + */ + public function setMessage($message) + { + $this->message = $message; + return $this; + } + + /** + * @return mixed|null + */ + public function getData() + { + return $this->data; + } + + /** + * @param mixed|null $data + * @return $this + */ + public function setData($data) + { + $this->data = $data; + return $this; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $result = [ + 'code' => $this->code, + 'message' => $this->message, + ]; + + if ($this->data !== null) { + $result['data'] = $this->data; + } + + return (object) $result; + } + + public static function isWellKnownErrorCode($code) + { + return isset(self::$errorMessages[$code]); + } + + public static function isCustomErrorCode($code) + { + return $code >= self::MIN_CUSTOM_ERROR && $code <= self::MAX_CUSTOM_ERROR; + } + + /** + * @deprecated please use jsonSerialize() + * @return mixed + */ + public function toPlainObject() + { + return $this->jsonSerialize(); + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php new file mode 100644 index 0000000..0c04ac7 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php @@ -0,0 +1,28 @@ +<?php + +namespace gipfl\Protocol\JsonRpc\Handler; + +use gipfl\Protocol\JsonRpc\Error; +use gipfl\Protocol\JsonRpc\Notification; +use gipfl\Protocol\JsonRpc\Request; + +class FailingPacketHandler implements JsonRpcHandler +{ + /** @var Error */ + protected $error; + + public function __construct(Error $error) + { + $this->error = $error; + } + + public function processNotification(Notification $notification) + { + // We silently ignore them + } + + public function processRequest(Request $request) + { + return $this->error; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php new file mode 100644 index 0000000..f64bc68 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php @@ -0,0 +1,23 @@ +<?php + +namespace gipfl\Protocol\JsonRpc\Handler; + +use gipfl\Protocol\JsonRpc\Error; +use gipfl\Protocol\JsonRpc\Notification; +use gipfl\Protocol\JsonRpc\Request; +use React\Promise\PromiseInterface; + +interface JsonRpcHandler +{ + /** + * @param Request $request + * @return Error|PromiseInterface|mixed + */ + public function processRequest(Request $request); + + /** + * @param Notification $notification + * @return void + */ + public function processNotification(Notification $notification); +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php new file mode 100644 index 0000000..6e0655b --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php @@ -0,0 +1,217 @@ +<?php + +namespace gipfl\Protocol\JsonRpc\Handler; + +use Exception; +use gipfl\Json\JsonSerialization; +use gipfl\OpenRpc\Reflection\MetaDataClass; +use gipfl\OpenRpc\Reflection\MetaDataMethod; +use gipfl\Protocol\JsonRpc\Error; +use gipfl\Protocol\JsonRpc\Notification; +use gipfl\Protocol\JsonRpc\Request; +use RuntimeException; +use TypeError; +use function call_user_func_array; +use function method_exists; +use function preg_split; +use function sprintf; +use function strpos; + +class NamespacedPacketHandler implements JsonRpcHandler +{ + protected $nsSeparator = '.'; + + protected $nsRegex = '/\./'; + + protected $handlers = []; + + /** + * @var MetaDataMethod[] + */ + protected $knownMethods = []; + + public function processNotification(Notification $notification) + { + list($namespace, $method) = $this->splitMethod($notification->getMethod()); + try { + $this->call($namespace, $method, $notification); + } catch (Exception $exception) { + // Well... we might want to log this + } catch (TypeError $exception) { + // Well... we might want to log this + } + } + + public function processRequest(Request $request) + { + list($namespace, $method) = $this->splitMethod($request->getMethod()); + + try { + return $this->call($namespace, $method, $request); + } catch (Exception $exception) { + return Error::forException($exception); + } catch (TypeError $error) { + return Error::forTypeError($error); + } + } + + /** + * @param string $namespace + * @param object $handler + */ + public function registerNamespace($namespace, $handler) + { + if (isset($this->handlers[$namespace])) { + throw new RuntimeException("Cannot register a namespace twice: '$namespace'"); + } + $this->handlers[$namespace] = $handler; + $this->analyzeNamespace($namespace, $handler); + } + + protected function analyzeNamespace($namespace, $handler) + { + $meta = MetaDataClass::analyze(get_class($handler)); + foreach ($meta->getMethods() as $method) { + $this->knownMethods[$namespace . $this->nsSeparator . $method->getName()] = $method; + } + } + + /** + * @param string $namespace + */ + public function removeNamespace($namespace) + { + unset($this->handlers[$namespace]); + } + + public function setNamespaceSeparator($separator) + { + $this->nsSeparator = $separator; + $this->nsRegex = '/' . preg_quote($separator, '/') . '/'; + + return $this; + } + + protected function call($namespace, $method, Notification $notification) + { + if (! isset($this->handlers[$namespace])) { + return $this->notFound($notification, ', no handler for ' . $namespace); + } + + $handler = $this->handlers[$namespace]; + if ($handler instanceof JsonRpcHandler) { + if ($notification instanceof Request) { + return $handler->processRequest($notification); + } else { + $handler->processNotification($notification); + } + } + + $params = $notification->getParams(); + if (! is_array($params)) { + try { + $params = $this->prepareParams($notification->getMethod(), $params); + } catch (Exception $e) { + return Error::forException($e); + } + } + if ($notification instanceof Request) { + $rpcMethod = $method . 'Request'; + if (is_callable([$handler, $rpcMethod])) { + return call_user_func_array([$handler, $rpcMethod], $params); + } + + return $this->notFound($notification, ', no ' . $rpcMethod); + } else { + $rpcMethod = $method . 'Notification'; + if (is_callable([$handler, $rpcMethod])) { + call_user_func_array([$handler, $rpcMethod], $params); + } + + return null; + } + } + + protected function prepareParams($method, $params) + { + if (! isset($this->knownMethods[$method])) { + throw new Exception('Cannot map params for unknown method'); + } + + $meta = $this->knownMethods[$method]; + $result = []; + foreach ($meta->getParameters() as $parameter) { + $name = $parameter->getName(); + if (property_exists($params, $name)) { + $value = $params->$name; + if ($value === null) { + // TODO: check if required + $result[] = $value; + continue; + } + switch ($parameter->getType()) { + case 'int': + $result[] = (int) $value; + break; + case 'float': + $result[] = (float) $value; + break; + case 'string': + $result[] = (string) $value; + break; + case 'array': + $result[] = (array) $value; + break; + case 'bool': + case 'boolean': + $result[] = (bool) $value; + break; + case 'object': + $result[] = (object) $value; + break; + default: + $type = $parameter->getType(); + if (class_exists($type)) { + foreach (class_implements($type) as $implement) { + if ($implement === JsonSerialization::class) { + $result[] = $type::fromSerialization($value); + break 2; + } + } + } + throw new Exception(sprintf( + 'Unsupported parameter type for %s: %s', + $method, + $parameter->getType() + )); + } + } else { + // TODO: isRequired? Set null + throw new Exception("Missing parameter for $method: $name"); + } + } + + return $result; + } + + protected function splitMethod($method) + { + if (strpos($method, $this->nsSeparator) === false) { + return [null, $method]; + } + + return preg_split($this->nsRegex, $method, 2); + } + + protected function notFound(Notification $notification, $suffix = '') + { + $error = new Error(Error::METHOD_NOT_FOUND); + $error->setMessage(sprintf( + '%s: %s' . $suffix, + $error->getMessage(), + $notification->getMethod() + )); + + return $error; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php b/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php new file mode 100644 index 0000000..88c6f5b --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php @@ -0,0 +1,241 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use Evenement\EventEmitterTrait; +use Exception; +use gipfl\Json\JsonEncodeException; +use gipfl\Protocol\JsonRpc\Handler\JsonRpcHandler; +use InvalidArgumentException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; +use React\Promise\Deferred; +use React\Promise\Promise; +use React\Stream\DuplexStreamInterface; +use React\Stream\Util; +use RuntimeException; +use function mt_rand; +use function React\Promise\reject; +use function React\Promise\resolve; + +class JsonRpcConnection implements LoggerAwareInterface +{ + use EventEmitterTrait; + use LoggerAwareTrait; + + /** @var DuplexStreamInterface */ + protected $connection; + + /** @var ?JsonRpcHandler */ + protected $handler; + + /** @var Deferred[] */ + protected $pending = []; + + protected $unknownErrorCount = 0; + + public function __construct(DuplexStreamInterface $connection, JsonRpcHandler $handler = null) + { + $this->setLogger(new NullLogger()); + $this->connection = $connection; + $this->setHandler($handler); + $this->connection->on('data', function ($data) { + try { + $this->handlePacket(Packet::decode($data)); + } catch (\Exception $error) { + $this->logger->error($error->getMessage()); + $this->unknownErrorCount++; + if ($this->unknownErrorCount === 3) { + // e.g.: decoding errors + // TODO: should we really close? Or just send error responses for every Exception? + $this->close(); + } + $response = new Response(); + $response->setError(Error::forException($error)); + $this->connection->write($response->toString()); + } + }); + $connection->on('close', function () { + $this->rejectAllPendingRequests('Connection closed'); + }); + // Hint: Util::pipe takes care of the pipe event + Util::forwardEvents($connection, $this, ['end', 'error', 'close', 'drain']); + } + + /** + * @param Packet $packet + */ + protected function handlePacket(Packet $packet) + { + if ($packet instanceof Response) { + $this->handleResponse($packet); + } elseif ($packet instanceof Request) { + if ($this->handler) { + $result = $this->handler->processRequest($packet); + } else { + $result = new Error(Error::METHOD_NOT_FOUND); + $result->setMessage($result->getMessage() . ': ' . $packet->getMethod()); + } + $this->sendResultForRequest($packet, $result); + } elseif ($packet instanceof Notification) { + if ($this->handler) { + $this->handler->processNotification($packet); + } + } else { + // Will not happen as long as there is no bug in Packet + throw new RuntimeException('Packet was neither Request/Notification nor Response'); + } + } + + protected function handleResponse(Response $response) + { + $id = $response->getId(); + if (isset($this->pending[$id])) { + $promise = $this->pending[$id]; + unset($this->pending[$id]); + $promise->resolve($response); + } else { + $this->handleUnmatchedResponse($response); + } + } + + protected function handleUnmatchedResponse(Response $response) + { + $this->logger->error('Unmatched Response: ' . $response->toString()); + } + + protected function sendResultForRequest(Request $request, $result) + { + if ($result instanceof Error) { + $response = Response::forRequest($request); + $response->setError($result); + if ($this->connection && $this->connection->isWritable()) { + $this->connection->write($response->toString()); + } else { + $this->logger->error('Failed to send response, have no writable connection'); + } + } elseif ($result instanceof Promise) { + $result->then(function ($result) use ($request) { + $this->sendResultForRequest($request, $result); + }, function ($error) use ($request) { + $response = Response::forRequest($request); + if ($error instanceof Exception || $error instanceof \Throwable) { + $response->setError(Error::forException($error)); + } else { + $response->setError(new Error(Error::INTERNAL_ERROR, $error)); + } + // TODO: Double-check, this used to loop + $this->connection->write($response->toString()); + })->done(); + } else { + $response = Response::forRequest($request); + $response->setResult($result); + if ($this->connection && $this->connection->isWritable()) { + $this->connection->write($response->toString()); + } else { + $this->logger->error('Failed to send response, have no writable connection'); + } + } + } + + /** + * @param Request $request + * @return \React\Promise\PromiseInterface + */ + public function sendRequest(Request $request) + { + $id = $request->getId(); + if ($id === null) { + $id = $this->getNextRequestId(); + $request->setId($id); + } + if (isset($this->pending[$id])) { + throw new InvalidArgumentException( + "A request with id '$id' is already pending" + ); + } + if (!$this->connection->isWritable()) { + return reject(new Exception('Cannot write to socket')); + } + try { + $this->connection->write($request->toString()); + } catch (JsonEncodeException $e) { + return reject($e->getMessage()); + } + $deferred = new Deferred(function () use ($id) { + unset($this->pending[$id]); + }); + $this->pending[$id] = $deferred; + + return $deferred->promise()->then(function (Response $response) { + if ($response->isError()) { + return reject(new RuntimeException($response->getError()->getMessage())); + } + + return resolve($response->getResult()); + }); + } + + public function request($method, $params = null) + { + return $this->sendRequest(new Request($method, $this->getNextRequestId(), $params)); + } + + protected function getNextRequestId() + { + for ($i = 0; $i < 100; $i++) { + $id = mt_rand(1, 1000000000); + if (!isset($this->pending[$id])) { + return $id; + } + } + + throw new RuntimeException('Unable to generate a free random request ID, gave up after 100 attempts'); + } + + /** + * @param Notification $packet + */ + public function sendNotification(Notification $packet) + { + $this->connection->write($packet->toString()); + } + + /** + * @param string $method + * @param null $params + */ + public function notification($method, $params = null) + { + $notification = new Notification($method, $params); + $this->sendNotification($notification); + } + + /** + * @param PacketHandler $handler + * @return $this + */ + public function setHandler(JsonRpcHandler $handler = null) + { + $this->handler = $handler; + return $this; + } + + protected function rejectAllPendingRequests($message) + { + foreach ($this->pending as $pending) { + $pending->reject(new Exception($message)); + } + $this->pending = []; + } + + public function close() + { + if ($this->connection) { + $this->connection->close(); + $this->handler = null; + $this->connection = null; + } + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Notification.php b/vendor/gipfl/protocol-jsonrpc/src/Notification.php new file mode 100644 index 0000000..338efc1 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Notification.php @@ -0,0 +1,98 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +class Notification extends Packet +{ + /** @var string */ + protected $method; + + /** @var \stdClass|array */ + protected $params; + + public function __construct($method, $params) + { + $this->setMethod($method); + $this->setParams($params); + } + + /** + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * @param string $method + */ + public function setMethod($method) + { + $this->method = $method; + } + + /** + * @return object|array + */ + public function getParams() + { + return $this->params; + } + + /** + * @param object|array $params + */ + public function setParams($params) + { + $this->params = $params; + } + + /** + * @param string $name + * @param mixed $default + * @return mixed|null + */ + public function getParam($name, $default = null) + { + $p = & $this->params; + if (\is_object($p) && \property_exists($p, $name)) { + return $p->$name; + } elseif (\is_array($p) && \array_key_exists($name, $p)) { + return $p[$name]; + } + + return $default; + } + + /** + * @return object + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $plain = [ + 'jsonrpc' => '2.0', + 'method' => $this->method, + 'params' => $this->params, + ]; + + if ($this->hasExtraProperties()) { + $plain += (array) $this->getExtraProperties(); + } + + return (object) $plain; + } + + /** + * @param $method + * @param $params + * @return static + */ + public static function create($method, $params) + { + $packet = new Notification($method, $params); + + return $packet; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Packet.php b/vendor/gipfl/protocol-jsonrpc/src/Packet.php new file mode 100644 index 0000000..8dca44e --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Packet.php @@ -0,0 +1,226 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use gipfl\Json\JsonException; +use gipfl\Json\JsonSerialization; +use gipfl\Json\JsonString; +use gipfl\Protocol\Exception\ProtocolError; +use function property_exists; + +abstract class Packet implements JsonSerialization +{ + /** @var \stdClass|null */ + protected $extraProperties; + + /** + * @return string + * @throws \gipfl\Json\JsonEncodeException + */ + public function toString() + { + return JsonString::encode($this->jsonSerialize()); + } + + /** + * @return string + * @throws \gipfl\Json\JsonEncodeException + */ + public function toPrettyString() + { + return JsonString::encode($this->jsonSerialize(), JSON_PRETTY_PRINT); + } + + /** + * @return bool + */ + public function hasExtraProperties() + { + return $this->extraProperties !== null; + } + + /** + * @return \stdClass|null + */ + public function getExtraProperties() + { + return $this->extraProperties; + } + + /** + * @param \stdClass|null $extraProperties + * @return $this + * @throws ProtocolError + */ + public function setExtraProperties($extraProperties) + { + foreach (['id', 'error', 'result', 'jsonrpc', 'method', 'params'] as $key) { + if (property_exists($extraProperties, $key)) { + throw new ProtocolError("Cannot accept '$key' as an extra property"); + } + } + $this->extraProperties = $extraProperties; + + return $this; + } + + /** + * @param string $name + * @param mixed|null $default + * @return mixed|null + */ + public function getExtraProperty($name, $default = null) + { + if (isset($this->extraProperties->$name)) { + return $this->extraProperties->$name; + } else { + return $default; + } + } + + + /** + * @param string $name + * @param mixed $value + * @return $this + */ + public function setExtraProperty($name, $value) + { + if ($this->extraProperties === null) { + $this->extraProperties = (object) [$name => $value]; + } else { + $this->extraProperties->$name = $value; + } + + return $this; + } + + /** + * @param $string + * @return Notification|Request|Response + * @throws ProtocolError + */ + public static function decode($string) + { + try { + return self::fromSerialization(JsonString::decode($string)); + } catch (JsonException $e) { + throw new ProtocolError(sprintf( + 'JSON decode failed: %s', + $e->getMessage() + ), Error::PARSE_ERROR); + } + } + + public static function fromSerialization($any) + { + $version = static::stripRequiredProperty($any, 'jsonrpc'); + if ($version !== '2.0') { + throw new ProtocolError( + "Only JSON-RPC 2.0 is supported, got $version", + Error::INVALID_REQUEST + ); + } + + // Hint: we MUST use property_exists here, as a NULL id is allowed + // in error responsed in case it wasn't possible to determine a + // request id + $hasId = property_exists($any, 'id'); + $id = static::stripOptionalProperty($any, 'id'); + $error = static::stripOptionalProperty($any, 'error'); + if (property_exists($any, 'method')) { + $method = static::stripRequiredProperty($any, 'method'); + $params = static::stripRequiredProperty($any, 'params'); + + if ($id === null) { + $packet = new Notification($method, $params); + } else { + $packet = new Request($method, $id, $params); + } + } elseif (! $hasId) { + throw new ProtocolError( + "Given string is not a valid JSON-RPC 2.0 response: id is missing", + Error::INVALID_REQUEST + ); + } else { + $packet = new Response($id); + if ($error) { + $packet->setError(new Error( + static::stripOptionalProperty($error, 'code'), + static::stripOptionalProperty($error, 'message'), + static::stripOptionalProperty($error, 'data') + )); + } else { + $result = static::stripRequiredProperty($any, 'result'); + $packet->setResult($result); + } + } + if (count((array) $any) > 0) { + $packet->setExtraProperties($any); + } + + return $packet; + } + + /** + * @param object $object + * @param string $property + * @throws ProtocolError + */ + protected static function assertPropertyExists($object, $property) + { + if (! property_exists($object, $property)) { + throw new ProtocolError( + "Expected valid JSON-RPC, got no '$property' property", + Error::INVALID_REQUEST + ); + } + } + + /** + * @param \stdClass $object + * @param string $property + * @return mixed|null + */ + protected static function stripOptionalProperty($object, $property) + { + if (property_exists($object, $property)) { + $value = $object->$property; + unset($object->$property); + + return $value; + } + + return null; + } + + /** + * @param \stdClass $object + * @param string $property + * @return mixed + * @throws ProtocolError + */ + protected static function stripRequiredProperty($object, $property) + { + if (! property_exists($object, $property)) { + throw new ProtocolError( + "Expected valid JSON-RPC, got no '$property' property", + Error::INVALID_REQUEST + ); + } + + $value = $object->$property; + unset($object->$property); + + return $value; + } + + /** + * @deprecated please use jsonSerialize() + * @return string + */ + public function toPlainObject() + { + return $this->jsonSerialize(); + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php new file mode 100644 index 0000000..e3f23c2 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php @@ -0,0 +1,11 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +/** + * @deprecated + */ +interface PacketHandler +{ + public function handle(Notification $notification); +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Request.php b/vendor/gipfl/protocol-jsonrpc/src/Request.php new file mode 100644 index 0000000..2061a41 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Request.php @@ -0,0 +1,59 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use gipfl\Protocol\Exception\ProtocolError; + +class Request extends Notification +{ + /** @var mixed */ + protected $id; + + /** + * Request constructor. + * @param string $method + * @param mixed $id + * @param null $params + */ + public function __construct($method, $id = null, $params = null) + { + parent::__construct($method, $params); + + $this->id = $id; + } + + /** + * @return object + * @throws ProtocolError + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + if ($this->id === null) { + throw new ProtocolError( + 'A request without an ID is not valid' + ); + } + + $plain = parent::jsonSerialize(); + $plain->id = $this->id; + + return $plain; + } + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * @param mixed $id + */ + public function setId($id) + { + $this->id = $id; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Response.php b/vendor/gipfl/protocol-jsonrpc/src/Response.php new file mode 100644 index 0000000..3a5ad90 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Response.php @@ -0,0 +1,128 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +class Response extends Packet +{ + /** @var mixed|null This could be null when sending a parse error */ + protected $id; + + /** @var mixed */ + protected $result; + + /** @var Error|null */ + protected $error; + + /** @var string */ + protected $message; + + public function __construct($id = null) + { + $this->id = $id; + } + + /** + * @param Request $request + * @return Response + */ + public static function forRequest(Request $request) + { + return new Response($request->getId()); + } + + /** + * @return object + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $plain = [ + 'jsonrpc' => '2.0', + ]; + if ($this->hasExtraProperties()) { + $plain += (array) $this->getExtraProperties(); + } + + if ($this->id !== null) { + $plain['id'] = $this->id; + } + + if ($this->error === null) { + $plain['result'] = $this->result; + } else { + if (! isset($plain['id'])) { + $plain['id'] = null; + } + $plain['error'] = $this->error; + } + + return (object) $plain; + } + + /** + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + /** + * @param $result + * @return $this + */ + public function setResult($result) + { + $this->result = $result; + + return $this; + } + + /** + * @return bool + */ + public function hasId() + { + return null !== $this->id; + } + + /** + * @return null|int|string + */ + public function getId() + { + return $this->id; + } + + /** + * @param $id + */ + public function setId($id) + { + $this->id = $id; + } + + public function isError() + { + return $this->error !== null; + } + + /** + * @return Error|null + */ + public function getError() + { + return $this->error; + } + + /** + * @param $error + * @return $this; + */ + public function setError(Error $error) + { + $this->error = $error; + + return $this; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/TestCase.php b/vendor/gipfl/protocol-jsonrpc/src/TestCase.php new file mode 100644 index 0000000..05f54ba --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/TestCase.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use PHPUnit\Framework\TestCase as BaseTestCase; +use React\EventLoop\LoopInterface; + +class TestCase extends BaseTestCase +{ + protected $examples = []; + + protected function parseExample($key) + { + return Packet::decode($this->examples[$key]); + } + + protected function failAfterSeconds($seconds, LoopInterface $loop) + { + $loop->addTimer($seconds, function () use ($seconds) { + throw new \RuntimeException("Timed out after $seconds seconds"); + }); + } + + protected function collectErrorsForNotices(&$errors) + { + \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$errors) { + if (\error_reporting() === 0) { // @-operator in use + return false; + } + $errors[] = new \ErrorException($errstr, 0, $errno, $errfile, $errline); + + return false; // Always continue with normal error processing + }, E_ALL | E_STRICT); + + \error_reporting(E_ALL | E_STRICT); + } + + protected function throwEventualErrors(array $errors) + { + foreach ($errors as $error) { + throw $error; + } + } +} diff --git a/vendor/gipfl/protocol-netstring/LICENSE b/vendor/gipfl/protocol-netstring/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/protocol-netstring/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/protocol-netstring/composer.json b/vendor/gipfl/protocol-netstring/composer.json new file mode 100644 index 0000000..0387e7d --- /dev/null +++ b/vendor/gipfl/protocol-netstring/composer.json @@ -0,0 +1,25 @@ +{ + "name": "gipfl/protocol-netstring", + "description": "Simple NetString stream wrapper", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Protocol\\NetString\\": "src" + } + }, + "require": { + "php": ">=5.4.0", + "ext-ctype": "*", + "gipfl/protocol": ">=0.2" + } +} diff --git a/vendor/gipfl/protocol-netstring/src/StreamWrapper.php b/vendor/gipfl/protocol-netstring/src/StreamWrapper.php new file mode 100644 index 0000000..b23b909 --- /dev/null +++ b/vendor/gipfl/protocol-netstring/src/StreamWrapper.php @@ -0,0 +1,113 @@ +<?php + +namespace gipfl\Protocol\NetString; + +use gipfl\Protocol\Exception\ProtocolError; +use gipfl\Protocol\Generic\AbstractStreamWrapper; + +class StreamWrapper extends AbstractStreamWrapper +{ + protected $buffer = ''; + protected $bufferLength = 0; + protected $bufferOffset = 0; + protected $expectedLength; + + public function close() + { + // We might want to complain when buffer is not empty + $this->buffer = ''; + $this->bufferLength = 0; + $this->bufferOffset = 0; + $this->expectedLength = null; + parent::close(); + } + + /** + * @param $data + */ + public function handleData($data) + { + $this->buffer .= $data; + $this->bufferLength += \strlen($data); + while ($this->bufferHasPacket()) { + $this->processNextPacket(); + + if ($this->bufferOffset !== 0) { + $this->buffer = \substr($this->buffer, $this->bufferOffset); + $this->bufferOffset = 0; + $this->bufferLength = \strlen($this->buffer); + } + } + } + + public function write($data) + { + return $this->output->write(strlen($data) . ':' . $data . ','); + } + + public function end($data = null) + { + if ($data !== null) { + $this->write($data); + } + + $this->output->end(); + } + + /** + * @return bool + */ + protected function bufferHasPacket() + { + if ($this->expectedLength === null) { + if (false !== ($pos = \strpos(\substr($this->buffer, $this->bufferOffset, 10), ':'))) { + $lengthString = \ltrim(\substr($this->buffer, $this->bufferOffset, $pos), ','); + if (! \ctype_digit($lengthString)) { + $this->emit('error', [ + new ProtocolError("Invalid length $lengthString") + ]); + $this->close(); + + return false; + } + $this->expectedLength = (int) $lengthString; + $this->bufferOffset = $pos + 1; + } elseif ($this->bufferLength > ($this->bufferOffset + 10)) { + $this->throwInvalidBuffer(); + $this->close(); + + return false; + } else { + return false; + } + } + + return $this->bufferLength > ($this->bufferOffset + $this->expectedLength); + } + + protected function processNextPacket() + { + $packet = \substr($this->buffer, $this->bufferOffset, $this->expectedLength); + + $this->bufferOffset = $this->bufferOffset + $this->expectedLength; + $this->expectedLength = null; + + $this->emit('data', [$packet]); + } + + protected function throwInvalidBuffer() + { + $len = \strlen($this->buffer); + if ($len < 200) { + $debug = $this->buffer; + } else { + $debug = \substr($this->buffer, 0, 100) + . \sprintf('[..] truncated %d bytes [..] ', $len) + . \substr($this->buffer, -100); + } + + $this->emit('error', [ + new ProtocolError("Got invalid NetString data: $debug") + ]); + } +} diff --git a/vendor/gipfl/protocol/LICENSE b/vendor/gipfl/protocol/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/protocol/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/protocol/composer.json b/vendor/gipfl/protocol/composer.json new file mode 100644 index 0000000..4c204f3 --- /dev/null +++ b/vendor/gipfl/protocol/composer.json @@ -0,0 +1,26 @@ +{ + "name": "gipfl/protocol", + "description": "Base library for some network protocol implementations", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Protocol\\Exception\\": "src/Exception", + "gipfl\\Protocol\\Generic\\": "src/Generic" + } + }, + "require": { + "php": ">=5.4.0", + "evenement/evenement": "^2", + "react/stream": "^1.0" + } +} diff --git a/vendor/gipfl/protocol/src/Exception/ProtocolError.php b/vendor/gipfl/protocol/src/Exception/ProtocolError.php new file mode 100644 index 0000000..b4a42ca --- /dev/null +++ b/vendor/gipfl/protocol/src/Exception/ProtocolError.php @@ -0,0 +1,9 @@ +<?php + +namespace gipfl\Protocol\Exception; + +use Exception; + +class ProtocolError extends Exception +{ +} diff --git a/vendor/gipfl/protocol/src/Generic/AbstractStreamWrapper.php b/vendor/gipfl/protocol/src/Generic/AbstractStreamWrapper.php new file mode 100644 index 0000000..12f6e82 --- /dev/null +++ b/vendor/gipfl/protocol/src/Generic/AbstractStreamWrapper.php @@ -0,0 +1,139 @@ +<?php + +namespace gipfl\Protocol\Generic; + +use Evenement\EventEmitterTrait; +use Exception; +use React\Stream\DuplexStreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; +use RuntimeException; + +abstract class AbstractStreamWrapper implements DuplexStreamInterface +{ + use EventEmitterTrait; + + /** @var ReadableStreamInterface */ + protected $input; + + /** @var WritableStreamInterface */ + protected $output; + + private $closed = false; + + public function __construct(ReadableStreamInterface $in, WritableStreamInterface $out = null) + { + $this->readFrom($in); + if ($out === null && $in instanceof WritableStreamInterface) { + $this->writeTo($in); + } else { + $this->writeTo($out); + } + } + + abstract public function handleData($data); + + protected function readFrom(ReadableStreamInterface $input) + { + $this->input = $input; + if (! $input->isReadable()) { + $this->close(); + return; + } + $input->on('data', function ($data) { + $this->handleData($data); + }); + $input->on('end', function () { + $this->handleEnd(); + }); + $input->on('close', function () { + $this->close(); + }); + $input->on('error', function (Exception $error) { + $this->handleError($error); + }); + } + + protected function writeTo(WritableStreamInterface $output) + { + $this->output = $output; + if (! $this->output->isWritable()) { + $this->close(); + throw new RuntimeException('Cannot write to output'); + } + + $output->on('drain', function () { + $this->handleDrain(); + }); + $output->on('close', function () { + $this->close(); + }); + $output->on('error', function (Exception $error) { + $this->handleError($error); + }); + } + + protected function handleDrain() + { + $this->emit('drain'); + } + + protected function handleEnd() + { + if (! $this->closed) { + $this->emit('end'); + $this->close(); + } + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function isWritable() + { + return !$this->closed && $this->output->isWritable(); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->input->close(); + $this->output->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = []) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + /** + * @param Exception $error + */ + protected function handleError(Exception $error) + { + $this->emit('error', [$error]); + $this->close(); + } +} diff --git a/vendor/gipfl/react-utils/LICENSE b/vendor/gipfl/react-utils/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/react-utils/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/react-utils/composer.json b/vendor/gipfl/react-utils/composer.json new file mode 100644 index 0000000..638b7fe --- /dev/null +++ b/vendor/gipfl/react-utils/composer.json @@ -0,0 +1,24 @@ +{ + "name": "gipfl/react-utils", + "description": "Useful ReactPHP-related helper classes and methods", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\ReactUtils\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "gipfl/log": ">=0.1" + } +} diff --git a/vendor/gipfl/react-utils/src/RetryUnless.php b/vendor/gipfl/react-utils/src/RetryUnless.php new file mode 100644 index 0000000..2d21123 --- /dev/null +++ b/vendor/gipfl/react-utils/src/RetryUnless.php @@ -0,0 +1,256 @@ +<?php + +namespace gipfl\ReactUtils; + +use Exception; +use gipfl\Log\DummyLogger; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; +use RuntimeException; + +class RetryUnless implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + /** @var LoopInterface */ + protected $loop; + + /** @var Deferred */ + protected $deferred; + + /** @var TimerInterface */ + protected $timer; + + /** @var callable */ + protected $callback; + + /** @var bool */ + protected $expectsSuccess; + + /** @var int Regular interval */ + protected $interval = 1; + + /** @var int|null Optional, interval will be changed after $burst attempts */ + protected $burst = null; + + /** @var int|null Interval after $burst attempts */ + protected $finalInterval = null; + + /** @var int Current attempt count */ + protected $attempts = 0; + + /** @var bool No attempts will be made while paused */ + protected $paused = false; + + protected $lastError; + + protected function __construct($callback, $expectsSuccess = true) + { + $this->setLogger(new DummyLogger()); + $this->callback = $callback; + $this->expectsSuccess = $expectsSuccess; + } + + public static function succeeding($callback) + { + return new static($callback); + } + + public static function failing($callback) + { + return new static($callback, false); + } + + public function run(LoopInterface $loop) + { + $this->assertNotRunning(); + $this->deferred = $deferred = new Deferred(); + $this->loop = $loop; + $loop->futureTick(function () { + $this->nextAttempt(); + }); + + return $deferred->promise(); + } + + public function getLastError() + { + return $this->lastError; + } + + public function setInterval($interval) + { + $this->interval = $interval; + + return $this; + } + + public function slowDownAfter($burst, $interval) + { + $this->burst = $burst; + $this->finalInterval = $interval; + + return $this; + } + + public function pause() + { + $this->removeEventualTimer(); + $this->paused = true; + + return $this; + } + + public function resume() + { + if ($this->paused) { + $this->paused = false; + if ($this->timer === null) { + $this->nextAttempt(); + } + } + } + + public function reset() + { + $this->attempts = 0; + $this->paused = false; + $this->removeEventualTimer(); + $this->rejectEventualDeferred('RetryUnless has been reset'); + + return $this; + } + + public function getAttempts() + { + return $this->attempts; + } + + protected function nextAttempt() + { + if ($this->paused) { + return; + } + + $this->removeEventualTimer(); + $this->attempts++; + try { + $callback = $this->callback; + $this->handleResult($callback()); + } catch (Exception $e) { + $this->handleResult($e); + } + } + + protected function logError(Exception $e) + { + if ($this->lastError !== $e->getMessage()) { + $this->lastError = $e->getMessage(); + // TODO: Support exceptions in our logger? + $this->logger->error($e->getMessage()); + } + } + + protected function handleResult($result) + { + if ($this->expectsSuccess) { + if ($result instanceof Exception) { + $this->logError($result); + $this->scheduleNextAttempt(); + } elseif ($result instanceof PromiseInterface) { + $later = function ($result) { + $this->handleResult($result); + }; + $result->then($later, $later); + } else { + $this->succeed($result); + } + } else { + if ($result instanceof Exception) { + $this->succeed($result); + } else { + $this->scheduleNextAttempt(); + } + } + } + + protected function scheduleNextAttempt() + { + if ($this->timer !== null) { + throw new RuntimeException( + 'RetryUnless schedules next attempt while already scheduled' + ); + } + $this->timer = $this->loop->addTimer($this->getNextInterval(), function () { + $this->nextAttempt(); + }); + } + + protected function succeed($result) + { + $this->removeEventualTimer(); + if ($this->deferred === null) { + $this->logger->warning('RetryUnless tries to resolve twice'); + + return; + } + $this->deferred->resolve($result); + $this->deferred = null; + $this->reset(); + } + + protected function getNextInterval() + { + if ($this->burst === null) { + return $this->interval; + } + + return $this->attempts >= $this->burst + ? $this->finalInterval + : $this->interval; + } + + protected function assertNotRunning() + { + if ($this->deferred) { + throw new RuntimeException( + 'Cannot re-run RetryUnless while already running' + ); + } + } + + protected function removeEventualTimer() + { + if ($this->timer) { + $this->loop->cancelTimer($this->timer); + $this->timer = null; + } + } + + protected function rejectEventualDeferred($reason) + { + if ($this->deferred !== null) { + $deferred = $this->deferred; + $this->deferred = null; + $deferred->reject($reason); + } + } + + public function cancel($reason = null) + { + $this->removeEventualTimer(); + $this->rejectEventualDeferred($reason ?: 'cancelled'); + } + + public function __destruct() + { + $this->removeEventualTimer(); + $this->rejectEventualDeferred('RetryUnless has been destructed'); + + $this->loop = null; + } +} diff --git a/vendor/gipfl/simple-daemon/composer.json b/vendor/gipfl/simple-daemon/composer.json new file mode 100644 index 0000000..1815081 --- /dev/null +++ b/vendor/gipfl/simple-daemon/composer.json @@ -0,0 +1,29 @@ +{ + "name": "gipfl/simple-daemon", + "description": "Run a simple daemon", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "gipfl\\SimpleDaemon\\": "src/" + } + }, + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "require": { + "php": ">=5.6.0", + "ext-pcntl": "*", + "gipfl/systemd": ">=0.3", + "react/promise": "^2", + "react/event-loop": ">=1.1", + "psr/log": ">=1.0", + "gipfl/json": ">=0.1", + "evenement/evenement": "*", + "gipfl/cli": ">=0.5", + "react/promise-timer": ">=1.5" + } +} diff --git a/vendor/gipfl/simple-daemon/src/Daemon.php b/vendor/gipfl/simple-daemon/src/Daemon.php new file mode 100644 index 0000000..8d08871 --- /dev/null +++ b/vendor/gipfl/simple-daemon/src/Daemon.php @@ -0,0 +1,156 @@ +<?php + +namespace gipfl\SimpleDaemon; + +use Exception; +use gipfl\Cli\Process; +use gipfl\SystemD\NotifySystemD; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use function React\Promise\resolve; +use function React\Promise\Timer\timeout; +use function sprintf; + +class Daemon implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + /** @var LoopInterface */ + private $loop; + + /** @var NotifySystemD|boolean */ + protected $systemd; + + /** @var DaemonTask[] */ + protected $daemonTasks = []; + + /** @var bool */ + protected $tasksStarted = false; + + /** @var bool */ + protected $reloading = false; + + public function run(LoopInterface $loop) + { + if ($this->logger === null) { + $this->setLogger(new NullLogger()); + } + $this->loop = $loop; + $this->registerSignalHandlers(); + $this->systemd = NotifySystemD::ifRequired($loop); + $loop->futureTick(function () { + if ($this->systemd) { + $this->systemd->setReady(); + } + + $this->startTasks(); + }); + } + + public function attachTask(DaemonTask $task) + { + if ($task instanceof LoggerAwareInterface) { + $task->setLogger($this->logger ?: new NullLogger()); + } + + $this->daemonTasks[] = $task; + if ($this->tasksStarted) { + $this->startTask($task); + } + } + + protected function startTasks() + { + $this->tasksStarted = true; + foreach ($this->daemonTasks as $task) { + $this->startTask($task); + } + } + + protected function startTask(DaemonTask $task) + { + if ($this->systemd && $task instanceof SystemdAwareTask) { + $task->setSystemd($this->systemd); + } + + $task->start($this->loop); + } + + protected function stopTasks() + { + if (empty($this->daemonTasks)) { + return resolve(); + } + + $deferred = new Deferred(); + foreach ($this->daemonTasks as $id => $task) { + $task->stop()->always(function () use ($id, $deferred) { + unset($this->daemonTasks[$id]); + if (empty($this->daemonTasks)) { + $this->tasksStarted = false; + $this->loop->futureTick(function () use ($deferred) { + $deferred->resolve(); + }); + } + }); + } + + return $deferred->promise(); + } + + protected function registerSignalHandlers() + { + $func = function ($signal) use (&$func) { + $this->shutdownWithSignal($signal, $func); + }; + $funcReload = function () { + $this->reload(); + }; + $this->loop->addSignal(SIGHUP, $funcReload); + $this->loop->addSignal(SIGINT, $func); + $this->loop->addSignal(SIGTERM, $func); + } + + protected function shutdownWithSignal($signal, &$func) + { + $this->loop->removeSignal($signal, $func); + $this->shutdown(); + } + + public function reload() + { + if ($this->reloading) { + $this->logger->error('Ignoring reload request, reload is already in progress'); + return; + } + $this->reloading = true; + $this->logger->notice('Stopping tasks, going gown for reload now'); + if ($this->systemd) { + $this->systemd->setReloading('Reloading the main process'); + } + $this->stopTasks()->then(function () { + $this->logger->notice('Everything stopped, restarting'); + Process::restart(); + }); + } + + public function shutdown() + { + timeout($this->stopTasks(), 5, $this->loop)->then(function () { + $this->loop->stop(); + }, function (Exception $e) { + if ($this->logger) { + $this->logger->error(sprintf( + 'Failed to safely shutdown, stopping anyways: %s', + $e->getMessage() + )); + } + $this->loop->addTimer(0.1, function () { + $this->loop->stop(); + }); + }); + } +} diff --git a/vendor/gipfl/simple-daemon/src/DaemonState.php b/vendor/gipfl/simple-daemon/src/DaemonState.php new file mode 100644 index 0000000..923a546 --- /dev/null +++ b/vendor/gipfl/simple-daemon/src/DaemonState.php @@ -0,0 +1,172 @@ +<?php + +namespace gipfl\SimpleDaemon; + +use Evenement\EventEmitterInterface; +use Evenement\EventEmitterTrait; +use gipfl\Json\JsonSerialization; +use function implode; +use function strlen; + +class DaemonState implements JsonSerialization, EventEmitterInterface +{ + use EventEmitterTrait; + + const ON_CHANGE = 'change'; + + /** @var string */ + protected $processTitle; + + /** @var ?string */ + protected $state; + + /** @var ?string */ + protected $currentProcessTitle; + + /** @var ?string */ + protected $currentMessage; + + /** @var string[] */ + protected $componentStates = []; + + /** + * @return string + */ + public function getProcessTitle() + { + return $this->processTitle; + } + + /** + * @param string $processTitle + * @return DaemonState + */ + public function setProcessTitle($processTitle) + { + $this->processTitle = $processTitle; + $this->refreshMessage(); + return $this; + } + + /** + * @return string|null + */ + public function getState() + { + return $this->state; + } + + /** + * @param string|null $state + * @return DaemonState + */ + public function setState($state) + { + $this->state = $state; + $this->refreshMessage(); + + return $this; + } + + /** + * @return string|null + */ + public function getCurrentMessage() + { + return $this->currentMessage; + } + + /** + * @return string[] + */ + public function getComponentStates() + { + return $this->componentStates; + } + + /** + * @param string[] $componentStates + * @return DaemonState + */ + public function setComponentStates($componentStates) + { + $this->componentStates = $componentStates; + $this->refreshMessage(); + return $this; + } + + /** + * @param string $name + * @param string $stateMessage + * @return $this + */ + public function setComponentState($name, $stateMessage) + { + if ($stateMessage === null) { + unset($this->componentStates[$name]); + } else { + $this->componentStates[$name] = $stateMessage; + } + $this->refreshMessage(); + + return $this; + } + + public function getComponentState($name) + { + if (isset($this->componentStates[$name])) { + return $this->componentStates[$name]; + } + + return null; + } + + public static function fromSerialization($any) + { + $self = new static(); + if (isset($any->state)) { + $self->state = $any->state; + } + if (isset($any->currentMessage)) { + $self->currentMessage = $any->currentMessage; + } + if (isset($any->processTitle)) { + $self->processTitle = $any->processTitle; + } + if (isset($any->components)) { + $self->componentStates = $any->components; + } + + return $self; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return (object) [ + 'state' => $this->state, + 'currentMessage' => $this->currentMessage, + 'processTitle' => $this->processTitle, + 'components' => $this->componentStates + ]; + } + + protected function refreshMessage() + { + $messageParts = []; + $state = $this->getState(); + if ($state !== null && strlen($state)) { + $messageParts[] = $state; + } + foreach ($this->getComponentStates() as $component => $message) { + $messageParts[] = "$component: $message"; + } + + $message = implode(', ', $messageParts); + if ($message !== $this->currentMessage || $this->processTitle !== $this->currentProcessTitle) { + $this->currentMessage = $message; + $this->currentProcessTitle = $this->processTitle; + $this->emit(self::ON_CHANGE, [$this->currentProcessTitle, $this->currentMessage]); + } + } +} diff --git a/vendor/gipfl/simple-daemon/src/DaemonTask.php b/vendor/gipfl/simple-daemon/src/DaemonTask.php new file mode 100644 index 0000000..70fc71a --- /dev/null +++ b/vendor/gipfl/simple-daemon/src/DaemonTask.php @@ -0,0 +1,20 @@ +<?php + +namespace gipfl\SimpleDaemon; + +use React\EventLoop\LoopInterface; +use React\Promise\ExtendedPromiseInterface; + +interface DaemonTask +{ + /** + * @param LoopInterface $loop + * @return ExtendedPromiseInterface + */ + public function start(LoopInterface $loop); + + /** + * @return ExtendedPromiseInterface + */ + public function stop(); +} diff --git a/vendor/gipfl/simple-daemon/src/SystemdAwareTask.php b/vendor/gipfl/simple-daemon/src/SystemdAwareTask.php new file mode 100644 index 0000000..3901d46 --- /dev/null +++ b/vendor/gipfl/simple-daemon/src/SystemdAwareTask.php @@ -0,0 +1,13 @@ +<?php + +namespace gipfl\SimpleDaemon; + +use gipfl\SystemD\NotifySystemD; + +interface SystemdAwareTask +{ + /** + * @param NotifySystemD $systemd + */ + public function setSystemd(NotifySystemD $systemd); +} diff --git a/vendor/gipfl/socket/composer.json b/vendor/gipfl/socket/composer.json new file mode 100644 index 0000000..cc19780 --- /dev/null +++ b/vendor/gipfl/socket/composer.json @@ -0,0 +1,29 @@ +{ + "name": "gipfl/socket", + "description": "Helpful ReactPHP socket utility classes", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "gipfl\\Socket\\": "src/" + } + }, + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "require": { + "php": ">=5.6.0", + "ext-posix": "*", + "ext-sockets": "*", + "evenement/evenement": ">=2.0", + "gipfl/json": ">=0.1", + "react/event-loop": ">=1.0", + "react/socket": ">=1.0" + } +} diff --git a/vendor/gipfl/socket/src/ConnectionList.php b/vendor/gipfl/socket/src/ConnectionList.php new file mode 100644 index 0000000..47ea489 --- /dev/null +++ b/vendor/gipfl/socket/src/ConnectionList.php @@ -0,0 +1,87 @@ +<?php + +namespace gipfl\Socket; + +use Evenement\EventEmitterInterface; +use Evenement\EventEmitterTrait; +use InvalidArgumentException; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Socket\ConnectionInterface; +use SplObjectStorage; +use function React\Promise\all; + +/** + * @method ConnectionInterface current + */ +class ConnectionList extends SplObjectStorage implements EventEmitterInterface +{ + use EventEmitterTrait; + + const ON_ATTACHED = 'attached'; + const ON_DETACHED = 'detached'; + + /** @var LoopInterface */ + protected $loop; + + public function __construct(LoopInterface $loop) + { + $this->loop = $loop; + } + + #[\ReturnTypeWillChange] + public function attach($object, $info = null) + { + if (! $object instanceof ConnectionInterface || $info !== null) { + throw new InvalidArgumentException(sprintf( + 'Can attach only %s instances', + ConnectionInterface::class + )); + } + $object->on('close', function () use ($object) { + $this->detach($object); + }); + + parent::attach($object, $info); + $this->emit(self::ON_ATTACHED, [$object]); + } + + #[\ReturnTypeWillChange] + public function detach($object) + { + parent::detach($object); + $this->emit(self::ON_DETACHED, [$object]); + } + + public function close() + { + $pending = []; + foreach ($this as $connection) { + $pending[] = $this->closeConnection($connection, $this->loop); + } + + return all($pending); + } + + public static function closeConnection(ConnectionInterface $connection, LoopInterface $loop, $timeout = 5) + { + $deferred = new Deferred(); + $connection->end(); + if ($connection->isWritable() || $connection->isReadable()) { + $timer = $loop->addTimer($timeout, function () use ($connection, $deferred) { + $connection->close(); + }); + + $connection->on('close', function () use ($deferred, $timer) { + $this->loop->cancelTimer($timer); + $deferred->resolve(); + }); + } else { + $loop->futureTick(function () use ($deferred) { + $deferred->resolve(); + }); + } + + return $deferred->promise(); + } +} diff --git a/vendor/gipfl/socket/src/UnixSocketInspection.php b/vendor/gipfl/socket/src/UnixSocketInspection.php new file mode 100644 index 0000000..e1e9819 --- /dev/null +++ b/vendor/gipfl/socket/src/UnixSocketInspection.php @@ -0,0 +1,89 @@ +<?php + +namespace gipfl\Socket; + +use RuntimeException; +use React\Socket\ConnectionInterface; +use function array_shift; +use function file_exists; +use function is_int; +use function posix_getgrgid; +use function posix_getpwuid; +use function preg_split; +use function socket_get_option; +use function socket_import_stream; +use function stat; +use function strlen; +use function trim; + +class UnixSocketInspection +{ + /** + * @param ConnectionInterface $connection + * @return UnixSocketPeer + */ + public static function getPeer(ConnectionInterface $connection) + { + $socket = socket_import_stream($connection->stream); + $remotePid = static::getRemotePidFromSocket($socket); + $stat = static::statProcFile($remotePid); + $uid = $stat['uid']; + $gid = $stat['gid']; + $userInfo = static::getUserInfo($uid); + $gecosParts = preg_split('/,/', $userInfo['gecos']); + $fullName = trim(array_shift($gecosParts)); + $groupInfo = static::getGroupInfo($gid); + + return new UnixSocketPeer( + $remotePid, + $uid, + $gid, + $userInfo['name'], + strlen($fullName) ? $fullName : null, + $groupInfo['name'] + ); + } + + protected static function getRemotePidFromSocket($socket) + { + // SO_PEERCRED = 17 + $remotePid = socket_get_option($socket, SOL_SOCKET, 17); + if (! is_int($remotePid) || ! $remotePid > 0) { + throw new RuntimeException("Remote PID expected, got " . var_export($remotePid)); + } + + return $remotePid; + } + + protected static function statProcFile($pid) + { + $procDir = "/proc/$pid"; + if (file_exists($procDir)) { + return stat($procDir); + } else { + throw new RuntimeException("Got no proc dir ($procDir) for remote node"); + } + } + + protected static function getUserInfo($uid) + { + $userInfo = posix_getpwuid($uid); + + if ($userInfo === false) { + throw new RuntimeException("Unable to resolve remote UID '$uid'"); + } + + return $userInfo; + } + + protected static function getGroupInfo($gid) + { + $groupInfo = posix_getgrgid($gid); + + if ($groupInfo === false) { + throw new RuntimeException("Unable to resolve remote GID '$gid'"); + } + + return $groupInfo; + } +} diff --git a/vendor/gipfl/socket/src/UnixSocketPeer.php b/vendor/gipfl/socket/src/UnixSocketPeer.php new file mode 100644 index 0000000..409487b --- /dev/null +++ b/vendor/gipfl/socket/src/UnixSocketPeer.php @@ -0,0 +1,102 @@ +<?php + +namespace gipfl\Socket; + +use gipfl\Json\JsonSerialization; + +class UnixSocketPeer implements JsonSerialization +{ + /** @var int */ + protected $pid; + + /** @var int */ + protected $uid; + + /** @var int */ + protected $gid; + + /** @var string */ + protected $username; + + /** @var ?string */ + protected $fullName; + + /** @var string */ + protected $groupName; + + public function __construct($pid, $uid, $gid, $username, $fullName, $groupName) + { + $this->pid = $pid; + $this->uid = $uid; + $this->gid = $gid; + $this->username = $username; + $this->fullName = $fullName; + $this->groupName = $groupName; + } + + /** + * @return int + */ + public function getPid() + { + return $this->pid; + } + + /** + * @return int + */ + public function getUid() + { + return $this->uid; + } + + /** + * @return int + */ + public function getGid() + { + return $this->gid; + } + + /** + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * @return string|null + */ + public function getFullName() + { + return $this->fullName; + } + + /** + * @return string + */ + public function getGroupName() + { + return $this->groupName; + } + + public static function fromSerialization($any) + { + return new static($any->pid, $any->uid, $any->gid, $any->username, $any->fullName, $any->groupName); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return (object) [ + 'pid' => $this->pid, + 'uid' => $this->uid, + 'gid' => $this->gid, + 'username' => $this->username, + 'fullName' => $this->fullName, + 'groupName' => $this->groupName, + ]; + } +} diff --git a/vendor/gipfl/stream/composer.json b/vendor/gipfl/stream/composer.json new file mode 100644 index 0000000..3bbcb6a --- /dev/null +++ b/vendor/gipfl/stream/composer.json @@ -0,0 +1,26 @@ +{ + "name": "gipfl/stream", + "description": "Helpful ReactPHP stream utility classes", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "gipfl\\Stream\\": "src/" + } + }, + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "require": { + "react/event-loop": ">=1.0", + "react/stream": ">=1.0" + }, + "require-dev": { + "gipfl/test": ">=0.1.1", + "phpunit/phpunit": "^9.3 || ^7.5 || ^6.5 || ^5.7", + "squizlabs/php_codesniffer": "^3.6" + } +} diff --git a/vendor/gipfl/stream/src/BufferedLineReader.php b/vendor/gipfl/stream/src/BufferedLineReader.php new file mode 100644 index 0000000..bd43155 --- /dev/null +++ b/vendor/gipfl/stream/src/BufferedLineReader.php @@ -0,0 +1,100 @@ +<?php + +namespace gipfl\Stream; + +use Evenement\EventEmitterTrait; +use React\EventLoop\LoopInterface; +use React\Stream\WritableStreamInterface; +use function strlen; +use function strpos; +use function substr; + +class BufferedLineReader implements WritableStreamInterface +{ + use EventEmitterTrait; + + /** @var LoopInterface */ + protected $loop; + + protected $buffer = ''; + + protected $writable = true; + + /** @var string */ + protected $separator; + + /** @var int */ + protected $separatorLength; + + protected $process; + + // protected $maxBufferSize; // Not yet. Do we need this? + + /** + * @param string $separator + * @param LoopInterface $loop + */ + public function __construct($separator, LoopInterface $loop) + { + $this->loop = $loop; + $this->separator = $separator; + $this->separatorLength = strlen($separator); + $this->process = function () { + $this->processBuffer(); + }; + } + + protected function processBuffer() + { + $lastPos = 0; + while (false !== ($pos = strpos($this->buffer, $this->separator, $lastPos))) { + $this->emit('line', [substr($this->buffer, $lastPos, $pos - $lastPos)]); + $lastPos = $pos + $this->separatorLength; + } + if ($lastPos !== 0) { + $this->buffer = substr($this->buffer, $lastPos); + } + } + + public function isWritable() + { + return $this->writable; + } + + public function write($data) + { + if (! $this->writable) { + return false; + } + $this->buffer .= $data; + if (strpos($data, $this->separator) !== false) { + $this->loop->futureTick($this->process); + } + + return true; + } + + public function end($data = null) + { + if ($data !== null) { + $this->buffer .= $data; + } + $this->close(); + } + + public function close() + { + $this->writable = false; + $this->processBuffer(); + $remainingBuffer = $this->buffer; + $this->buffer = ''; + if ($length = strlen($remainingBuffer)) { + $this->emit('error', [new \Exception(sprintf( + 'There are %d unprocessed bytes in our buffer: %s', + $length, + substr($remainingBuffer, 0, 64) + ))]); + } + $this->emit('close'); + } +} diff --git a/vendor/gipfl/systemd/LICENSE b/vendor/gipfl/systemd/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/systemd/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/systemd/composer.json b/vendor/gipfl/systemd/composer.json new file mode 100644 index 0000000..d751bee --- /dev/null +++ b/vendor/gipfl/systemd/composer.json @@ -0,0 +1,26 @@ +{ + "name": "gipfl/systemd", + "description": "SystemD-related library", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\SystemD\\": "src" + } + }, + "require": { + "php": ">=5.6.3", + "ext-posix": "*", + "ext-sockets": "*", + "react/event-loop": "^1.0" + } +} diff --git a/vendor/gipfl/systemd/src/NotificationSocket.php b/vendor/gipfl/systemd/src/NotificationSocket.php new file mode 100644 index 0000000..fe4a687 --- /dev/null +++ b/vendor/gipfl/systemd/src/NotificationSocket.php @@ -0,0 +1,122 @@ +<?php + +namespace gipfl\SystemD; + +use RuntimeException; +use function file_exists; +use function is_writable; + +class NotificationSocket +{ + /** @var resource */ + protected $socket; + + public function __construct($path) + { + if (@file_exists($path) && is_writable($path)) { + $this->path = $path; + } else { + throw new RuntimeException("Unix Socket '$path' is not writable"); + } + + $this->connect(); + } + + /** + * Send custom parameters to systemd + * + * This is for internal use only, but might be used to test new functionality + * + * @param array $params + * @internal + */ + public function send(array $params) + { + $message = $this->buildMessage($params); + $length = \strlen($message); + $result = @socket_send($this->socket, $message, $length, 0); + if ($result === false) { + $error = \socket_last_error($this->socket); + + throw new RuntimeException( + "Failed to send to SystemD: " . \socket_strerror($error), + $error + ); + } + if ($result !== $length) { + throw new RuntimeException( + "Wanted to send $length Bytes to SystemD, only $result have been sent" + ); + } + } + + /** + * Get the path to the the systemd notification socket + * + * Usually /run/systemd/notify or similar + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Transforms a key/value array into a string like "key1=val1\nkey2=val2" + * + * @param array $params + * @return string + */ + protected function buildMessage(array $params) + { + $message = ''; + foreach ($params as $key => $value) { + $message .= "$key=$value\n"; + } + + return $message; + } + + /** + * Connect to the discovered socket + * + * Will be /run/systemd/notify or similar. No async logic, as this + * shouldn't block. If systemd blocks we're dead anyways, so who cares + */ + protected function connect() + { + $path = $this->path; + $socket = @\socket_create(AF_UNIX, SOCK_DGRAM, 0); + if ($socket === false) { + throw new RuntimeException('Unable to create socket'); + } + + if (! @\socket_connect($socket, $path)) { + $error = \socket_last_error($socket); + + throw new RuntimeException( + "Unable to connect to unix domain socket $path: " . \socket_strerror($error), + $error + ); + } + + $this->socket = $socket; + } + + /** + * Disconnect the socket if connected + */ + public function disconnect() + { + if (\is_resource($this->socket)) { + @\socket_close($this->socket); + $this->socket = null; + } + } + + public function __destruct() + { + $this->disconnect(); + } +} diff --git a/vendor/gipfl/systemd/src/NotifySystemD.php b/vendor/gipfl/systemd/src/NotifySystemD.php new file mode 100644 index 0000000..0561fd6 --- /dev/null +++ b/vendor/gipfl/systemd/src/NotifySystemD.php @@ -0,0 +1,292 @@ +<?php + +namespace gipfl\SystemD; + +use Exception; +use InvalidArgumentException; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; +use RuntimeException; + +class NotifySystemD +{ + /** @var LoopInterface */ + protected $loop; + + /** @var TimerInterface */ + protected $timer; + + /** @var string|null */ + protected $path; + + /** @var string|null */ + protected $status; + + /** @var float */ + protected $interval; + + /** @var bool */ + protected $reloading = false; + + /** @var bool */ + protected $ready = false; + + /** @var bool */ + protected $failed = false; + + /** @var string|null The Invocation ID (128bit UUID) if available */ + protected $invocationId; + + protected $notificationSocket; + + /** + * NotifySystemD constructor. + * @param string $notifySocket + * @param int $intervalSecs + */ + public function __construct($notifySocket, $intervalSecs = 1) + { + $this->interval = $intervalSecs; + $this->notificationSocket = new NotificationSocket($notifySocket); + } + + /** + * Starts sending WatchDog pings + * + * @param LoopInterface $loop + */ + public function run(LoopInterface $loop) + { + $this->loop = $loop; + $ping = function () { + try { + $this->pingWatchDog(); + } catch (Exception $e) { + printf( + "<%d>Failed to ping systemd watchdog: %s\n", + LOG_ERR, + $e->getMessage() + ); + } + }; + $loop->futureTick($ping); + $this->timer = $loop->addPeriodicTimer($this->interval, $ping); + + return $this; + } + + /** + * Stop sending watchdog notifications + * + * Usually there is no need to do so, and the destructor does this anyways + */ + public function stop() + { + if ($this->timer !== null) { + $this->loop->cancelTimer($this->timer); + } + } + + public static function ifRequired(LoopInterface $loop, $env = null) + { + if ($env === null) { + $env = $_SERVER; + } + + if (! systemd::startedThisProcess($env)) { + return false; + } + + if (isset($env['WATCHDOG_USEC'])) { + $interval = $env['WATCHDOG_USEC'] / 2000000; // Half of that time + } else { + $interval = 1; + } + + return (new static($env['NOTIFY_SOCKET'], $interval)) + ->eventuallySetInvocationIdFromEnv($env) + ->run($loop); + } + + /** + * Extend the Watchdog timeout + * + * Useful to inform systemd before slow startup/shutdown operations. This + * is available since systemd v236. Older versions silently ignore this. + * + * @param float $seconds + */ + public function extendTimeout($seconds) + { + $this->send(['EXTEND_TIMEOUT_USEC' => (int) $seconds * 1000000]); + } + + /** + * Send a notification to the systemd watchdog + */ + public function pingWatchDog() + { + $this->send(['WATCHDOG' => '1']); + } + + /** + * Set the (visible) service status + * + * @param $status + */ + public function setStatus($status) + { + if ($status !== $this->status) { + $this->status = $status; + $this->send(['STATUS' => $status]); + } + } + + public function setReloading($status = null) + { + $this->ready = false; + $this->status = $status; + $params = ['RELOADING' => '1']; + if ($status !== null) { + $params['STATUS'] = $status; + } + $this->send($params); + } + + public function setError($error, $status = null) + { + $this->ready = false; + $this->status = $status; + if ($error instanceof Exception) { + $errNo = $error->getCode(); + $status = $status ?: $error->getMessage(); + } elseif (\is_int($error)) { + $errNo = $error; + } else { + throw new InvalidArgumentException( + 'Error has to be an Exception or an Integer' + ); + } + $params = []; + if ($status !== null) { + $params['STATUS'] = $status; + $this->status = $status; + } + + $params = ['ERRNO' => (string) $errNo]; + $this->send($params); + $this->failed = true; + } + + public function setReady($status = null) + { + $this->ready = true; + $params = [ + 'READY' => '1', + 'MAINPID' => \posix_getpid(), + ]; + + if ($status !== null) { + $params['STATUS'] = $status; + $this->status = $status; + } + + $this->send($params); + } + + /** + * Returns a 128bit uuid (16 hex characters) Invocation ID if available, + * null in case there isn't + * + * @return string|null + */ + public function getInvocationId() + { + return $this->invocationId; + } + + /** + * Whether we got an Invocation ID + * + * @return bool + */ + public function hasInvocationId() + { + return $this->invocationId !== null; + } + + /** + * Get the path to the the systemd notification socket + * + * Usually /run/systemd/notify or similar + * + * @return string + */ + public function getSocketPath() + { + return $this->notificationSocket->getPath(); + } + + /** + * Our Watchdog interval in seconds + * + * This is a float value: half of WATCHDOG_USEC if given - otherwise 1 second + * + * @return float + */ + public function getWatchdogInterval() + { + return $this->interval; + } + + /** + * Send custom parameters to systemd + * + * This is for internal use only, but might be used to test new functionality + * + * @param array $params + * @internal + */ + public function send(array $params) + { + if ($this->failed) { + throw new RuntimeException('Cannot notify SystemD after failing'); + } + + $this->notificationSocket->send($params); + } + + /** + * If INVOCATION_ID is available in the given ENV array: keep it + * + * Fails in case we do not get an 128bit string + * + * @param array $env + * @return $this + */ + protected function eventuallySetInvocationIdFromEnv(array $env) + { + $key = 'INVOCATION_ID'; + if (isset($env[$key])) { + if (\strlen($env[$key]) === 32) { + $this->invocationId = $env[$key]; + } else { + throw new RuntimeException(sprintf( + 'Unsupported %s="%s"', + $key, + $env['$key'] + )); + } + } + + return $this; + } + + public function __destruct() + { + $this->notificationSocket->disconnect(); + $this->notificationSocket = null; + $this->stop(); + unset($this->loop); + } +} diff --git a/vendor/gipfl/systemd/src/systemd.php b/vendor/gipfl/systemd/src/systemd.php new file mode 100644 index 0000000..7894a8f --- /dev/null +++ b/vendor/gipfl/systemd/src/systemd.php @@ -0,0 +1,19 @@ +<?php + +namespace gipfl\SystemD; + +class systemd +{ + /** + * @param null $env + * @return bool + */ + public static function startedThisProcess($env = null) + { + if ($env === null) { + $env = $_SERVER; + } + + return isset($env['NOTIFY_SOCKET']); + } +} diff --git a/vendor/gipfl/translation/LICENSE b/vendor/gipfl/translation/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/translation/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/translation/composer.json b/vendor/gipfl/translation/composer.json new file mode 100644 index 0000000..35608bb --- /dev/null +++ b/vendor/gipfl/translation/composer.json @@ -0,0 +1,20 @@ +{ + "name": "gipfl/translation", + "description": "Translation helpers", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "require": { + "php": ">=5.4.0" + }, + "autoload": { + "psr-4": { + "gipfl\\Translation\\": "src" + } + } +} diff --git a/vendor/gipfl/translation/src/NoTranslator.php b/vendor/gipfl/translation/src/NoTranslator.php new file mode 100644 index 0000000..3ea64ef --- /dev/null +++ b/vendor/gipfl/translation/src/NoTranslator.php @@ -0,0 +1,11 @@ +<?php + +namespace gipfl\Translation; + +class NoTranslator implements TranslatorInterface +{ + public function translate($string) + { + return $string; + } +} diff --git a/vendor/gipfl/translation/src/StaticTranslator.php b/vendor/gipfl/translation/src/StaticTranslator.php new file mode 100644 index 0000000..d06572b --- /dev/null +++ b/vendor/gipfl/translation/src/StaticTranslator.php @@ -0,0 +1,31 @@ +<?php + +namespace gipfl\Translation; + +class StaticTranslator +{ + /** @var TranslatorInterface */ + private static $translator; + + public static function get() + { + if (self::$translator === null) { + static::setNoTranslator(); + } + + return static::$translator; + } + + public static function setNoTranslator() + { + static::set(new NoTranslator()); + } + + /** + * @param TranslatorInterface $translator + */ + public static function set(TranslatorInterface $translator) + { + self::$translator = $translator; + } +} diff --git a/vendor/gipfl/translation/src/TranslationHelper.php b/vendor/gipfl/translation/src/TranslationHelper.php new file mode 100644 index 0000000..02cfdfc --- /dev/null +++ b/vendor/gipfl/translation/src/TranslationHelper.php @@ -0,0 +1,37 @@ +<?php + +namespace gipfl\Translation; + +trait TranslationHelper +{ + /** @var TranslatorInterface */ + private static $translator; + + /** + * @param $string + * @param string|null $context + * @return string + */ + public function translate($string, $context = null) + { + return self::getTranslator()->translate($string); + } + + public static function getTranslator() + { + return StaticTranslator::get(); + } + + public static function setNoTranslator() + { + StaticTranslator::set(new NoTranslator()); + } + + /** + * @param TranslatorInterface $translator + */ + public static function setTranslator(TranslatorInterface $translator) + { + StaticTranslator::set($translator); + } +} diff --git a/vendor/gipfl/translation/src/TranslatorInterface.php b/vendor/gipfl/translation/src/TranslatorInterface.php new file mode 100644 index 0000000..8026953 --- /dev/null +++ b/vendor/gipfl/translation/src/TranslatorInterface.php @@ -0,0 +1,8 @@ +<?php + +namespace gipfl\Translation; + +interface TranslatorInterface +{ + public function translate($string); +} diff --git a/vendor/gipfl/translation/src/WrapTranslator.php b/vendor/gipfl/translation/src/WrapTranslator.php new file mode 100644 index 0000000..d17fcbd --- /dev/null +++ b/vendor/gipfl/translation/src/WrapTranslator.php @@ -0,0 +1,26 @@ +<?php + +namespace gipfl\Translation; + +class WrapTranslator implements TranslatorInterface +{ + /** @var callable */ + private $callback; + + /** @var TranslatorInterface */ + private $wrapped; + + public function __construct(TranslatorInterface $wrapped, callable $callback) + { + $this->wrapped = $wrapped; + $this->callback = $callback; + } + + public function translate($string) + { + return call_user_func_array( + $this->callback, + [$this->wrapped->translate($string)] + ); + } +} diff --git a/vendor/gipfl/web/LICENSE b/vendor/gipfl/web/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/web/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/web/composer.json b/vendor/gipfl/web/composer.json new file mode 100644 index 0000000..ce28dd5 --- /dev/null +++ b/vendor/gipfl/web/composer.json @@ -0,0 +1,25 @@ +{ + "name": "gipfl/web", + "description": "Various web widgets", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Web\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "ipl/html": ">=0.3", + "gipfl/translation": ">=0.1.1" + } +} diff --git a/vendor/gipfl/web/public/css/01-gipfl-base.less b/vendor/gipfl/web/public/css/01-gipfl-base.less new file mode 100644 index 0000000..c352858 --- /dev/null +++ b/vendor/gipfl/web/public/css/01-gipfl-base.less @@ -0,0 +1,8 @@ +@gipfl-color-low-sat-blue: #dae3e6; +// map Icinga vars, to avoid warnings in other files +@gipfl-color-main: @icinga-blue; +@gipfl-color-text: @text-color; +@gipfl-color-gray-light: @gray-light; +@gipfl-bg-element: #f2f1f0; +@gipfl-bg-element: @low-sat-blue; +@gipfl-icon-font: 'ifont'; diff --git a/vendor/gipfl/web/public/css/21-gipfl-collapsible.less b/vendor/gipfl/web/public/css/21-gipfl-collapsible.less new file mode 100644 index 0000000..da065c1 --- /dev/null +++ b/vendor/gipfl/web/public/css/21-gipfl-collapsible.less @@ -0,0 +1,55 @@ +/** + * Lightweight support for small collapsible components + * + * @since v0.6.0 + */ +.gipfl-collapsible-control { + display: block; + border-bottom: 1px solid @gipfl-color-gray-light; + &:hover { + cursor: pointer; + text-decoration: none; + } + &::after { + font-family: @gipfl-icon-font; + content: '\f103'; + float: right; + margin-right: 0.5em; + color: @gipfl-color-gray-light; + } + &:hover { + &::after { + color: @gipfl-color-text; + } + } + &:focus { + text-decoration: none; + &::after { + color: @gipfl-color-main; + } + } +} + +.gipfl-collapsible .collapsed { + :not(.gipfl-collapsible-control) { + display: none; + } + .gipfl-collapsible-control { + &::after { + content: '\e87a'; + } + } +} + +ul.gipfl-collapsible { + list-style-type: none; + margin: 0; + padding: 0; + li { + margin: 0; + } + .gipfl-collapsible-control { + font-weight: bold; + line-height: 2em; + } +} diff --git a/vendor/gipfl/web/public/css/21-gipfl-widget-hint.less b/vendor/gipfl/web/public/css/21-gipfl-widget-hint.less new file mode 100644 index 0000000..2a4f9f9 --- /dev/null +++ b/vendor/gipfl/web/public/css/21-gipfl-widget-hint.less @@ -0,0 +1,57 @@ +/** + * State Hints + * + * @since v0.6.0 + */ +div.gipfl-widget-hint { + border: 1px solid @text-color; + padding: 0.5em; + line-height: 2em; + max-width: 60em; + border-left-width: 3em; + border-radius: 0.25em; + margin-bottom: 1em; + background-color: @body-bg-color; + box-shadow: fade(@text-color, 30%) 1em 0 1.5em 0; + &:before { + position: relative; + margin-left: -1.4em; + margin-right: 0.5em; + height: 100%; + vertical-align: middle; + font-family: 'ifont'; + color: white; + font-size: 2em; + } + &.ok { + border-color: @color-ok; + box-shadow: fade(@color-ok, 30%) 1em 0 1.5em 0; + } + &.info { + border-color: @color-pending; + box-shadow: fade(@color-pending, 30%) 1em 0 1.5em 0; + } + &.warning { + border-color: @color-warning; + box-shadow: fade(@color-warning, 30%) 1em 0 1.5em 0; + } + &.error { + border-color: @color-critical; + box-shadow: fade(@color-critical, 30%) 1em 0 1.5em 0; + } + &.critical:before, &.error:before { + content: '\e881'; + } + &.warning:before { + content: '\e881'; + } + &.info:before { + content: '\e87d'; + } + &.ok:before { + content: '\e803'; + } + a { + text-decoration: underline; + } +} diff --git a/vendor/gipfl/web/public/css/31-gipfl-name-value-table.less b/vendor/gipfl/web/public/css/31-gipfl-name-value-table.less new file mode 100644 index 0000000..30ae217 --- /dev/null +++ b/vendor/gipfl/web/public/css/31-gipfl-name-value-table.less @@ -0,0 +1,27 @@ +table.gipfl-name-value-table { + width: 100%; + > tbody > tr > th { + color: @text-color-light; + // Reset default font-weight + font-weight: bold; + padding-left: 0; + text-align: right; + vertical-align: top; + width: 12em; + } + > tbody > tr > td { + vertical-align: top; + } +} + +table.gipfl-name-value-table a { + color: @icinga-blue; +} + +table.gipfl-name-value-table pre { + background-color: transparent; + margin: 0; + padding: 0; + font-size: unset; + display: inline; +} diff --git a/vendor/gipfl/web/public/css/40-gipfl-form.less b/vendor/gipfl/web/public/css/40-gipfl-form.less new file mode 100644 index 0000000..cf6722e --- /dev/null +++ b/vendor/gipfl/web/public/css/40-gipfl-form.less @@ -0,0 +1,69 @@ +form.gipfl-form { + input[type="submit"] { + margin-right: 0.5em; + } + input[type="submit"]:first-of-type { + background-color: @icinga-blue; + color: @text-color-inverted; + border: 2px solid @icinga-blue; + font-weight: bold; + } + + select:not([multiple]) { + padding-right: 1.5625em; + background-image: url(../img/select-icon.svg); + background-repeat: no-repeat; + background-position: right center; + background-size: contain; + } + + input.validated { + background-color: fade(@color-ok, 30%); + border-color: @color-ok; + } + + :not(output):-moz-ui-invalid, :not(output).invalid { + background-color: fade(@color-critical, 30%); + border-color: @color-critical; + box-shadow: none; + } + + input[type=text].input-with-button { + max-width: 30em; + min-width: 18em; + width: 80%; + margin: 0; + border-top-right-radius: unset; + border-bottom-right-radius: unset; + border-right-style: none; + &:focus { + border-right-style: none; + } + } + + input[type=submit].input-element-related-button { + width: 20%; + max-width: 6em; + background-color: @icinga-blue; + color: @text-color-inverted; + border: 1px solid @icinga-blue; + border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + -webkit-border-radius: 0 3px 3px 0; + line-height: 2em; + height: 2.4em; + padding: 0; + margin: 0 1em 0 0; + &:hover { + background-color: @text-color-inverted; + color: @icinga-blue; + border-color: @icinga-blue; + } + } + p.gipfl-widget-hint { + max-width: 52.5em; + } + p.gipfl-element-description { + max-width: 36em; + } +} diff --git a/vendor/gipfl/web/public/css/41-director-form.less b/vendor/gipfl/web/public/css/41-director-form.less new file mode 100644 index 0000000..b907593 --- /dev/null +++ b/vendor/gipfl/web/public/css/41-director-form.less @@ -0,0 +1,339 @@ +form.gipfl-form { + label { + line-height: 2em; + } + dl { + margin: 0; + padding: 0; + &.active { + dt label { + text-decoration: underline; + } + } + } + + dt { + padding: 0; + margin: 0.5em 0 0 0; + display: inline-block; + vertical-align: top; + min-width: 8em; + max-width: 16em; + min-height: 2.5em; + width: 27%; + label { + color: @text-color; + font-weight: bold; + width: 12em; + font-size: inherit; + + &:hover { + text-decoration: underline; + cursor: pointer; + } + } + + &.errors label { + color: @color-critical; + } + } + + dd { + padding: 0.3em 0.5em; + margin: 0; + display: inline-block; + width: 73%; + min-height: 2.5em; + vertical-align: top; + + &.errors { + input[type=text], select { + border-color: @color-critical; + } + } + + &.full-width { + padding: 0.5em; + width: 100%; + } + + &:after { + display: block; + content: ''; + } + + ul.errors, ul.gipfl-form-element-errors { + list-style-type: none; + padding-left: 0.3em; + + li { + color: @colorCritical; + padding: 0.3em; + } + } + } + + input[type="text"], + input[type="password"], + input[type="number"], + input[type="datetime-local"], + input[type="date"], + input[type="time"], + textarea, + select { + background-color: @gipfl-bg-element; + } + + .errors label { + color: @color-critical; + } + + textarea { + height: auto; + max-width: 100%; + } + + input[type=file] { + background-color: @text-color-inverted; + padding-right: 1em; + } + + input[type=submit] { + .button(); + transition: none; // Avoid flickering on autosubmit + border-width: 1px; + margin-top: 0.5em; + + &:disabled { + border-color: @gray-light; + // background-color: @gray-light; + // color: #fff; + cursor: wait; + } + &:first-of-type { + border-width: 2px; + } + } + + select { + border: 1px solid #ddd; + cursor: pointer; + } + + input, + select, + select option, + textarea { + -webkit-appearance: none; + -moz-appearance: none; + } + + select::-ms-expand, + input::-ms-expand, + textarea::-ms-expand { // for IE 11 + display: none; + } + + input[type=submit].link-button { + color: @icinga-blue; + background: none; + border: none; + font-weight: normal; + padding: 0; + margin: 0; + text-align: left; + + &:hover { + text-decoration: underline; + } + } + + ul.form-errors { + list-style-type: none; + margin-bottom: 0.5em; + padding: 0; + + ul.errors { + list-style-type: none; + padding: 0; + } + + ul.errors li { + background: @color-critical; + font-weight: bold; + padding: 0.5em 1em; + color: white; + } + } + + input[type=text], input[type=password], textarea, select { + max-width: 36em; + min-width: 20em; + width: 100%; + } + input:not([type=submit]), + textarea, + select { + line-height: 2em; + height: 2.4em; + padding-left: 0.5em; + border-style: solid; + border-color: @gray-lighter; + border-width: 1px; + border-radius: 3px; + color: @text-color; + + &:hover { + border-color: @gray; + } + + &:focus, &:focus:hover { + border-color: @icinga-blue; + outline: none; + } + } + // duplicated from 40 -> ordering problem + input.validated { + background-color: fade(@color-ok, 30%); + border-color: @color-ok; + } + textarea { + height: unset; + min-height: 2.4em; + resize: vertical; + } + + input.search { + background-image: url("../img/icons/search.png"); + background-repeat: no-repeat; + padding-left: 2em; + } + + select[multiple], select[multiple=multiple] { + height: auto; + } + + select option { + height: 2em; + padding-top: 0.3em; + } + + #_FAKE_SUBMIT { + position: absolute; + left: -100%; + } + + select::-moz-focus-inner { + border: 0; + } + + select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #000; + } + + select[value=""] { + color: blue; + border: 1px solid #666; + // background-color: white; + } + + select option { + color: inherit; + padding-left: 0.5em; + } + + // default option + select option[value=""] { + // color: #aaa; + } + + fieldset { + margin: 0; + min-width: 36em; + + padding: 0 0 1.5em 0; + border: none; + + legend { + margin: 0 0 0.5em 0; + font-size: 1em; + border-bottom: 1px solid @gray-light; + font-weight: bold; + display: block; + width: 100%; + padding-left: 1em; + line-height: 2em; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + + &:hover { + border-color: @text-color; + } + + &::before { + // icon: down-dir + font-family: 'ifont'; + content: '\e81d'; + margin-left: -1em; + padding-top: 0; + float: left; + color: inherit; + } + } + + &.collapsed { + dl, dd, dt, ul, div { + display: none; + } + + legend { + margin: 0; + } + legend::before { + // icon: right-dir + content: '\e820'; + } + + margin-bottom: 0.2em; + padding-bottom: 0; + } + } +} + +#layout.minimal-layout div.content form.gipfl-form { + dt, dd { + display: block; + width: auto; + } + + dt label { + color: @text-color; + } + + fieldset { + min-width: unset; + } + + input[type=text], input[type=password], textarea, select { + max-width: unset; + min-width: unset; + border-color: @gray-light; + } + + dd.active { + input[type=text], input[type=password], textarea, select { + border-color: @icinga-blue; + } + } + + fieldset.collapsed { + dd, dt, ul, div { + display: none; + } + } +} + diff --git a/vendor/gipfl/web/public/css/42-director-extensible-set.less b/vendor/gipfl/web/public/css/42-director-extensible-set.less new file mode 100644 index 0000000..c5380cd --- /dev/null +++ b/vendor/gipfl/web/public/css/42-director-extensible-set.less @@ -0,0 +1,33 @@ +#layout.minimal-layout div.content form.gipfl-form { + ul.extensible-set { + max-width: unset; + border: 1px solid @gray-light; + } + + dd.active ul.extensible-set { + border: 1px solid @icinga-blue; + + input[type=submit]:first-of-type { + border-width: 1px; + } + } + + dd input.related-action[type='submit'] { + display: none; + } + + dd.active li.active input.related-action[type='submit'] { + display: inline-block; + } + + dd.active ul.extensible-set, ul.extensible-set.sortable { + input[type=text], select { + width: 100%; + } + + input[type=text] { + // background-color: white; + border: 1px solid white; + } + } +} diff --git a/vendor/gipfl/web/public/css/43-inline-form.less b/vendor/gipfl/web/public/css/43-inline-form.less new file mode 100644 index 0000000..10d214c --- /dev/null +++ b/vendor/gipfl/web/public/css/43-inline-form.less @@ -0,0 +1,29 @@ +form.gipfl-inline-form { + display: inline-block; + select { + width: auto; + min-width: unset; + max-width: unset; + border: none; + padding: 0 0.5em; + margin: 0; + line-height: unset; + box-sizing:border-box; + height: 1.5em; + + &:hover { + border: none; + } + + &:focus { + border: none; + } + } + input[type=submit] { + padding: 0 0.25em; + margin: 0 1em 0 0; + border: none; + line-height: unset; + box-sizing: border-box; + } +} diff --git a/vendor/gipfl/web/public/css/81-phpdiff.less b/vendor/gipfl/web/public/css/81-phpdiff.less new file mode 100644 index 0000000..bc634b1 --- /dev/null +++ b/vendor/gipfl/web/public/css/81-phpdiff.less @@ -0,0 +1,97 @@ +.Differences { + width: 100%; + table-layout: fixed; + empty-cells: show; +} + +.Differences thead { + display: none; +} + +.Differences thead th { + text-align: left; + padding-left: 4 / 14 * 16em; +} +.Differences tbody th { + text-align: right; + width: 4em; + padding: 1px 2px; + border-right: 1px solid @gray-light; + background: @gray-lightest; + font-weight: normal; + vertical-align: top; +} + +.Differences tbody td { + width: 50%; + .preformatted(); + word-break: break-all; +} + +@color-diff-ins: #bfb; +@color-diff-del: #faa; +@color-diff-changed-old: #fdd; +@color-diff-changed-new: #efe; + +.DifferencesSideBySide { + ins, del { + text-decoration: none; + } + + .ChangeInsert { + td.Left { + background: @gray-lighter; + } + td.Right { + background: @color-diff-ins; + } + } + + .ChangeDelete { + td.Left { + background: @color-diff-del; + } + td.Right { + background: @gray-lighter; + } + } + + .ChangeReplace { + td.Left { + background: @color-diff-changed-old; + del { + background: @color-diff-del; + } + } + + td.Right { + background: @color-diff-changed-new; + ins { + background: @color-diff-ins; + } + } + + } +} + +.Differences .Skipped { + background: @gray-lightest; +} + +.DifferencesInline .ChangeReplace .Left, +.DifferencesInline .ChangeDelete .Left { + background: #fdd; +} + +.DifferencesInline .ChangeReplace .Right, +.DifferencesInline .ChangeInsert .Right { + background: #dfd; +} + +.DifferencesInline .ChangeReplace ins { + background: #9e9; +} + +.DifferencesInline .ChangeReplace del { + background: #e99; +} diff --git a/vendor/gipfl/web/public/js/module.js b/vendor/gipfl/web/public/js/module.js new file mode 100644 index 0000000..83b19a5 --- /dev/null +++ b/vendor/gipfl/web/public/js/module.js @@ -0,0 +1,69 @@ +(function(window, $) { + 'use strict'; + + var Web = function () { + }; + + Web.prototype = { + initialize: function (icinga) { + this.icinga = icinga; + $(document).on('focus', 'form.gipfl-form input, form.gipfl-form textarea, form.gipfl-form select', this.formElementFocus); + $(document).on('click', '.gipfl-collapsible-control', this.toggleCollapsible); + }, + + toggleCollapsible: function (ev) { + var $toggle = $(ev.currentTarget); + var $collapsible = $toggle.parent(); + $collapsible.toggleClass('collapsed'); + }, + + formElementFocus: function (ev) { + var $input = $(ev.currentTarget); + if ($input.closest('form.editor').length) { + return; + } + var $set = $input.closest('.extensible-set'); + if ($set.length) { + var $textInputs = $('input[type=text]', $set); + if ($textInputs.length > 1) { + $textInputs.not(':first').attr('tabIndex', '-1'); + } + } + + var $dd = $input.closest('dd'); + if ($dd.attr('id') && $dd.attr('id').match(/button/)) { + return; + } + var $li = $input.closest('li'); + var $dt = $dd.prev(); + var $form = $dd.closest('form'); + + $form.find('dt, dd, dl, li').removeClass('active'); + $li.addClass('active'); + $dt.addClass('active'); + $dd.addClass('active'); + $dt.closest('dl').addClass('active'); + }, + + highlightFormErrors: function ($container) { + $container.find('dd ul.errors').each(function (idx, ul) { + var $ul = $(ul); + var $dd = $ul.closest('dd'); + var $dt = $dd.prev(); + + $dt.addClass('errors'); + $dd.addClass('errors'); + }); + }, + + toggleFieldset: function (ev) { + ev.stopPropagation(); + var $fieldset = $(ev.currentTarget).closest('fieldset'); + $fieldset.toggleClass('collapsed'); + this.fixFieldsetInfo($fieldset); + this.openedFieldsets[$fieldset.attr('id')] = ! $fieldset.hasClass('collapsed'); + } + }; + + window.incubatorComponentLoader.addComponent(new Web()); +})(window, jQuery); diff --git a/vendor/gipfl/web/src/Form.php b/vendor/gipfl/web/src/Form.php new file mode 100644 index 0000000..e5e52f9 --- /dev/null +++ b/vendor/gipfl/web/src/Form.php @@ -0,0 +1,281 @@ +<?php + +namespace gipfl\Web; + +use Exception; +use gipfl\Web\Form\Decorator\DdDtDecorator; +use gipfl\Web\Form\Validator\AlwaysFailValidator; +use gipfl\Web\Form\Validator\PhpSessionBasedCsrfTokenValidator; +use gipfl\Web\Widget\Hint; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Contract\FormElement; +use ipl\Html\Error; +use ipl\Html\Form as iplForm; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\FormElement\HiddenElement; +use ipl\Html\Html; +use RuntimeException; +use function array_key_exists; +use function get_class; +use function parse_str; + +class Form extends iplForm +{ + protected $formNameElementName = '__FORM_NAME'; + + protected $useCsrf = true; + + protected $useFormName = true; + + protected $defaultDecoratorClass = DdDtDecorator::class; + + protected $formCssClasses = ['gipfl-form']; + + /** @var boolean|null */ + protected $hasBeenSubmitted; + + public function ensureAssembled() + { + if ($this->hasBeenAssembled === false) { + if ($this->getRequest() === null) { + throw new RuntimeException('Cannot assemble a WebForm without a Request'); + } + $this->registerGipflElementLoader(); + $this->setupStyling(); + parent::ensureAssembled(); + $this->prepareWebForm(); + } + + return $this; + } + + protected function registerGipflElementLoader() + { + $this->addElementLoader(__NAMESPACE__ . '\\Form\\Element'); + } + + public function setSubmitted($submitted = true) + { + $this->hasBeenSubmitted = (bool) $submitted; + + return $this; + } + + public function hasBeenSubmitted() + { + if ($this->hasBeenSubmitted === null) { + return parent::hasBeenSubmitted(); + } else { + return $this->hasBeenSubmitted; + } + } + + public function disableCsrf() + { + $this->useCsrf = false; + + return $this; + } + + public function doNotCheckFormName() + { + $this->useFormName = false; + + return $this; + } + + protected function prepareWebForm() + { + if ($this->hasElement($this->formNameElementName)) { + return; // Called twice + } + if ($this->useFormName) { + $this->addFormNameElement(); + } + if ($this->useCsrf && $this->getMethod() === 'POST') { + $this->addCsrfElement(); + } + } + + protected function getUniqueFormName() + { + return get_class($this); + } + + protected function addFormNameElement() + { + $element = new HiddenElement($this->formNameElementName, [ + 'value' => $this->getUniqueFormName(), + 'ignore' => true, + ]); + $this->prepend($element); + $this->registerElement($element); + } + + public function addHidden($name, $value = null, $attributes = []) + { + if (is_array($value) && empty($attributes)) { + $attributes = $value; + $value = null; + } elseif ($value === null && is_scalar($attributes)) { + $value = $attributes; + $attributes = []; + } + if ($value !== null) { + $attributes['value'] = $value; + } + $element = new HiddenElement($name, $attributes); + $this->prepend($element); + $this->registerElement($element); + } + + public function registerElement(FormElement $element) + { + $idPrefix = ''; + if ($element instanceof BaseHtmlElement) { + if (! $element->getAttributes()->has('id')) { + $element->addAttributes(['id' => $idPrefix . $element->getName()]); + } + } + + return parent::registerElement($element); + } + + public function setElementValue($element, $value) + { + $this->wantFormElement($element)->setValue($value); + } + + public function getElementValue($elementName, $defaultValue = null) + { + $value = $this->getElement($elementName)->getValue(); + if ($value === null) { + return $defaultValue; + } else { + return $value; + } + } + + public function hasElementValue($elementName) + { + if ($this->hasElement($elementName)) { + return $this->getElement($elementName)->hasValue(); + } else { + return false; + } + } + + /** + * @param $element + * @return FormElement + */ + protected function wantFormElement($element) + { + if ($element instanceof BaseFormElement) { + return $element; + } else { + return $this->getElement($element); + } + } + + public function triggerElementError($element, $message, ...$params) + { + if (! empty($params)) { + $message = Html::sprintf($message, $params); + } + + $element = $this->wantFormElement($this->getElement($element)); + $element->addValidators([ + new AlwaysFailValidator(['message' => $message]) + ]); + } + + protected function setupStyling() + { + $this->setSeparator("\n"); + $this->addAttributes(['class' => $this->formCssClasses]); + if ($this->defaultDecoratorClass !== null) { + $this->setDefaultElementDecorator(new $this->defaultDecoratorClass); + } + } + + protected function addCsrfElement() + { + $element = new HiddenElement('__CSRF__', [ + 'ignore' => true, + ]); + $element->setValidators([ + new PhpSessionBasedCsrfTokenValidator() + ]); + // prepend / register -> avoid decorator + $this->prepend($element); + $this->registerElement($element); + if ($this->hasBeenSent()) { + if (! $element->isValid()) { + $element->setValue(PhpSessionBasedCsrfTokenValidator::generateCsrfValue()); + } + } else { + $element->setValue(PhpSessionBasedCsrfTokenValidator::generateCsrfValue()); + } + } + + public function getSentValue($name, $default = null) + { + $params = $this->getSentValues(); + + if (array_key_exists($name, $params)) { + return $params[$name]; + } else { + return $default; + } + } + + public function getSentValues() + { + $request = $this->getRequest(); + if ($request === null) { + throw new RuntimeException( + "It's impossible to access SENT values with no request" + ); + } + + if ($request->getMethod() === 'POST') { + $params = $request->getParsedBody(); + } elseif ($this->getMethod() === 'GET') { + parse_str($request->getUri()->getQuery(), $params); + } else { + $params = []; + } + + return $params; + } + + protected function onError() + { + $messages = $this->getMessages(); + if (empty($messages)) { + return; + } + $errors = []; + foreach ($this->getMessages() as $message) { + if ($message instanceof Exception) { + $this->prepend(Error::show($message)); + } else { + $errors[] = $message; + } + } + if (! empty($errors)) { + $this->prepend(Hint::error(implode(', ', $errors))); + } + } + + public function hasBeenSent() + { + if (parent::hasBeenSent()) { + return !$this->useFormName || $this->getSentValue($this->formNameElementName) + === $this->getUniqueFormName(); + } else { + return false; + } + } +} diff --git a/vendor/gipfl/web/src/Form/Decorator/DdDtDecorator.php b/vendor/gipfl/web/src/Form/Decorator/DdDtDecorator.php new file mode 100644 index 0000000..e5deae4 --- /dev/null +++ b/vendor/gipfl/web/src/Form/Decorator/DdDtDecorator.php @@ -0,0 +1,158 @@ +<?php + +namespace gipfl\Web\Form\Decorator; + +use gipfl\Web\HtmlHelper; +use ipl\Html\BaseHtmlElement; +use ipl\Html\FormDecorator\DecoratorInterface; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; + +class DdDtDecorator extends BaseHtmlElement implements DecoratorInterface +{ + const CSS_CLASS_ELEMENT_HAS_ERRORS = 'gipfl-form-element-has-errors'; + + const CSS_CLASS_ELEMENT_ERRORS = 'gipfl-form-element-errors'; + + const CSS_CLASS_DESCRIPTION = 'gipfl-element-description'; + + protected $tag = 'dl'; + + protected $dt; + + protected $dd; + + /** @var BaseFormElement */ + protected $element; + + /** @var HtmlDocument */ + protected $elementDoc; + + /** + * @param BaseFormElement $element + * @return static + */ + public function decorate(BaseFormElement $element) + { + $decorator = clone($this); + $decorator->element = $element; + $decorator->elementDoc = new HtmlDocument(); + $decorator->elementDoc->add($element); + // if (! $element instanceof HiddenElement) { + $element->prependWrapper($decorator); + + return $decorator; + } + + protected function prepareLabel() + { + $element = $this->element; + $label = $element->getLabel(); + if ($label === null || \strlen($label) === 0) { + return null; + } + + // Set HTML element.id to element name unless defined + if ($element->getAttributes()->has('id')) { + $attributes = ['for' => $element->getAttributes()->get('id')->getValue()]; + } else { + $attributes = null; + } + + if ($element->isRequired()) { + $label = [$label, Html::tag('span', ['aria-hidden' => 'true'], '*')]; + } + + return Html::tag('label', $attributes, $label); + } + + public function getAttributes() + { + $attributes = parent::getAttributes(); + + // TODO: only when sent?! + if ($this->element->hasBeenValidated() && ! $this->element->isValid()) { + HtmlHelper::addClassOnce($attributes, static::CSS_CLASS_ELEMENT_HAS_ERRORS); + } + + return $attributes; + } + + protected function prepareDescription() + { + if ($this->element) { + $description = $this->element->getDescription(); + if ($description !== null && \strlen($description)) { + return Html::tag('p', ['class' => static::CSS_CLASS_DESCRIPTION], $description); + } + } + + return null; + } + + protected function prepareErrors() + { + $errors = []; + foreach ($this->element->getMessages() as $message) { + $errors[] = Html::tag('li', $message); + } + + if (empty($errors)) { + return null; + } else { + return Html::tag('ul', ['class' => static::CSS_CLASS_ELEMENT_ERRORS], $errors); + } + } + + public function add($content) + { + // Our wrapper implementation automatically adds the wrapped element but + // we already do so in assemble() + if ($content !== $this->element) { + parent::add($content); + } + + return $this; + } + + protected function assemble() + { + $this->add([$this->dt(), $this->dd()]); + } + + public function getElementDocument() + { + return $this->elementDoc; + } + + public function dt() + { + if ($this->dt === null) { + $this->dt = Html::tag('dt', null, $this->prepareLabel()); + } + + return $this->dt; + } + + /** + * @return \ipl\Html\HtmlElement + */ + public function dd() + { + if ($this->dd === null) { + $this->dd = Html::tag('dd', null, [ + $this->getElementDocument(), + $this->prepareErrors(), + $this->prepareDescription() + ]); + } + + return $this->dd; + } + + public function __destruct() + { + $this->wrapper = null; + } +} diff --git a/vendor/gipfl/web/src/Form/Element/Boolean.php b/vendor/gipfl/web/src/Form/Element/Boolean.php new file mode 100644 index 0000000..dc5f85f --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/Boolean.php @@ -0,0 +1,39 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use gipfl\Translation\TranslationHelper; +use ipl\Html\FormElement\SelectElement; + +class Boolean extends SelectElement +{ + use TranslationHelper; + + public function __construct($name, $attributes = null) + { + parent::__construct($name, $attributes); + $options = [ + 'y' => $this->translate('Yes'), + 'n' => $this->translate('No'), + ]; + if (! $this->isRequired()) { + $options = [ + null => $this->translate('- please choose -'), + ] + $options; + } + + $this->setOptions($options); + } + + public function setValue($value) + { + if ($value === 'y' || $value === true) { + return parent::setValue('y'); + } elseif ($value === 'n' || $value === false) { + return parent::setValue('n'); + } + + // Hint: this will fail + return parent::setValue($value); + } +} diff --git a/vendor/gipfl/web/src/Form/Element/MultiSelect.php b/vendor/gipfl/web/src/Form/Element/MultiSelect.php new file mode 100644 index 0000000..07e2e9e --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/MultiSelect.php @@ -0,0 +1,119 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use ipl\Html\Attributes; +use ipl\Html\FormElement\SelectElement; + +class MultiSelect extends SelectElement +{ + protected $value = []; + + public function __construct($name, $attributes = null) + { + // Make sure we set value last as it depends on options + if (isset($attributes['value'])) { + $value = $attributes['value']; + unset($attributes['value']); + $attributes['value'] = $value; + } + + parent::__construct($name, $attributes); + + $this->getAttributes()->add('multiple', true); + } + + protected function registerValueCallback(Attributes $attributes) + { + $attributes->registerAttributeCallback( + 'value', + null, + [$this, 'setValue'] + ); + } + + public function getNameAttribute() + { + return $this->getName() . '[]'; + } + + public function setValue($value) + { + if (empty($value)) { // null, '', [] + $values = []; + } else { + $values = (array) $value; + } + $invalid = []; + foreach ($values as $val) { + if ($option = $this->getOption($val)) { + if ($option->getAttributes()->has('disabled')) { + $invalid[] = $val; + } + } else { + $invalid[] = $val; + } + } + if (count($invalid) > 0) { + $this->failForValues($invalid); + return $this; + } + + $this->value = $values; + $this->valid = null; + $this->updateSelection(); + + return $this; + } + + protected function failForValues($values) + { + $this->valid = false; + if (count($values) === 1) { + $value = array_shift($values); + $this->addMessage("'$value' is not allowed here"); + } else { + $valueString = implode("', '", $values); + $this->addMessage("'$valueString' are not allowed here"); + } + } + + public function validate() + { + /** + * @TODO(lippserd): {@link SelectElement::validate()} doesn't work here because isset checks fail with + * illegal offset type errors since our value is an array. It would make sense to decouple the classes to + * avoid having to copy code from the base class. + * Also note that {@see setValue()} already performs most of the validation. + */ + if ($this->isRequired() && empty($this->getValue())) { + $this->valid = false; + } else { + /** + * Copied from {@link \ipl\Html\BaseHtmlElement::validate()}. + */ + $this->valid = $this->getValidators()->isValid($this->getValue()); + $this->addMessages($this->getValidators()->getMessages()); + } + } + + public function updateSelection() + { + foreach ($this->options as $value => $option) { + if (in_array($value, $this->value)) { + $option->getAttributes()->add('selected', true); + } else { + $option->getAttributes()->remove('selected'); + } + } + + return $this; + } + + protected function assemble() + { + foreach ($this->options as $option) { + $this->add($option); + } + } +} diff --git a/vendor/gipfl/web/src/Form/Element/Password.php b/vendor/gipfl/web/src/Form/Element/Password.php new file mode 100644 index 0000000..b6f148e --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/Password.php @@ -0,0 +1,11 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use ipl\Html\FormElement\TextElement; + +class Password extends TextElement +{ + // TODO + protected $type = 'password'; +} diff --git a/vendor/gipfl/web/src/Form/Element/TextWithActionButton.php b/vendor/gipfl/web/src/Form/Element/TextWithActionButton.php new file mode 100644 index 0000000..13ebfb8 --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/TextWithActionButton.php @@ -0,0 +1,104 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use gipfl\Web\Form\Decorator\DdDtDecorator; +use ipl\Html\Attributes; +use ipl\Html\Form; +use ipl\Html\FormElement\SubmitElement; +use ipl\Html\FormElement\TextElement; + +class TextWithActionButton +{ + /** @var SubmitElement */ + protected $button; + + /** @var TextElement */ + protected $element; + + protected $buttonSuffix = '_related_button'; + + /** @var string */ + protected $elementName; + + /** @var array|Attributes */ + protected $elementAttributes; + + /** @var array|Attributes */ + protected $buttonAttributes; + + protected $elementClasses = ['input-with-button']; + + protected $buttonClasses = ['input-element-related-button']; + + /** + * TextWithActionButton constructor. + * @param string $elementName + * @param array|Attributes $elementAttributes + * @param array|Attributes $buttonAttributes + */ + public function __construct($elementName, $elementAttributes, $buttonAttributes) + { + $this->elementName = $elementName; + $this->elementAttributes = $elementAttributes; + $this->buttonAttributes = $buttonAttributes; + } + + public function addToForm(Form $form) + { + $button = $this->getButton(); + $form->registerElement($button); + $element = $this->getElement(); + $form->addElement($element); + /** @var DdDtDecorator $deco */ + $deco = $element->getWrapper(); + if ($deco instanceof DdDtDecorator) { + $deco->addAttributes(['position' => 'relative'])->getElementDocument()->add($button); + } + } + + public function getElement() + { + if ($this->element === null) { + $this->element = $this->createTextElement( + $this->elementName, + $this->elementAttributes + ); + } + + return $this->element; + } + + public function getButton() + { + if ($this->button === null) { + $this->button = $this->createSubmitElement( + $this->elementName . $this->buttonSuffix, + $this->buttonAttributes + ); + } + + return $this->button; + } + + protected function createTextElement($name, $attributes = null) + { + $element = new TextElement($name, $attributes); + $element->addAttributes([ + 'class' => $this->elementClasses, + ]); + + return $element; + } + + protected function createSubmitElement($name, $attributes = null) + { + $element = new SubmitElement($name, $attributes); + $element->addAttributes([ + 'formnovalidate' => true, + 'class' => $this->buttonClasses, + ]); + + return $element; + } +} diff --git a/vendor/gipfl/web/src/Form/Feature/NextConfirmCancel.php b/vendor/gipfl/web/src/Form/Feature/NextConfirmCancel.php new file mode 100644 index 0000000..81885d6 --- /dev/null +++ b/vendor/gipfl/web/src/Form/Feature/NextConfirmCancel.php @@ -0,0 +1,153 @@ +<?php + +namespace gipfl\Web\Form\Feature; + +use gipfl\Web\Form; +use ipl\Html\DeferredText; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\FormElement\SubmitElement; +use ipl\Html\HtmlDocument; +use ipl\Html\ValidHtml; + +class NextConfirmCancel +{ + /** @var SubmitElement */ + protected $next; + + /** @var SubmitElement */ + protected $confirm; + + /** @var SubmitElement */ + protected $cancel; + + protected $withNext; + + protected $withNextContent; + + protected $withConfirm; + + protected $withConfirmContent; + + protected $confirmFirst = true; + + public function __construct(SubmitElement $next, SubmitElement $confirm, SubmitElement $cancel) + { + $this->next = $next; + $this->confirm = $confirm; + $this->cancel = $cancel; + $this->withNextContent = new HtmlDocument(); + $this->withNext = new DeferredText(function () { + return $this->withNextContent; + }); + $this->withNext->setEscaped(); + + $this->withConfirmContent = new HtmlDocument(); + $this->withConfirm = new DeferredText(function () { + return $this->withConfirmContent; + }); + $this->withConfirm->setEscaped(); + } + + public function showWithNext($content) + { + $this->withNextContent->add($content); + } + + public function showWithConfirm($content) + { + $this->withConfirmContent->add($content); + } + + /** + * @param ValidHtml $html + * @param array $found Internal parameter + * @return BaseFormElement[] + */ + protected function pickFormElements(ValidHtml $html, &$found = []) + { + if ($html instanceof BaseFormElement) { + $found[] = $html; + } elseif ($html instanceof HtmlDocument) { + foreach ($html->getContent() as $content) { + $this->pickFormElements($content, $found); + } + } + + return $found; + } + + /** + * @param string $label + * @param array $attributes + * @return SubmitElement + */ + public static function buttonNext($label, $attributes = []) + { + return new SubmitElement('next', $attributes + [ + 'label' => $label + ]); + } + + /** + * @param string $label + * @param array $attributes + * @return SubmitElement + */ + public static function buttonConfirm($label, $attributes = []) + { + return new SubmitElement('submit', $attributes + [ + 'label' => $label + ]); + } + + /** + * @param string $label + * @param array $attributes + * @return SubmitElement + */ + public static function buttonCancel($label, $attributes = []) + { + return new SubmitElement('cancel', $attributes + [ + 'label' => $label + ]); + } + + public function addToForm(Form $form) + { + $cancel = $this->cancel; + $confirm = $this->confirm; + $next = $this->next; + if ($form->hasBeenSent()) { + $form->add($this->withConfirm); + if ($this->confirmFirst) { + $form->addElement($confirm); + $form->addElement($cancel); + } else { + $form->addElement($cancel); + $form->addElement($confirm); + } + if ($cancel->hasBeenPressed()) { + $this->withConfirmContent = new HtmlDocument(); + // HINT: we might also want to redirect on cancel and stop here, + // but currently we have no Response + $form->setSubmitted(false); + $form->remove($confirm); + $form->remove($cancel); + $form->add($next); + $form->setSubmitButton($next); + } else { + $form->setSubmitButton($confirm); + $form->remove($next); + foreach ($this->pickFormElements($this->withConfirmContent) as $element) { + $form->registerElement($element); + } + } + } else { + $form->add($this->withNext); + foreach ($this->pickFormElements($this->withNextContent) as $element) { + $form->registerElement($element); + } + $form->addElement($next); + } + } +} diff --git a/vendor/gipfl/web/src/Form/Validator/AlwaysFailValidator.php b/vendor/gipfl/web/src/Form/Validator/AlwaysFailValidator.php new file mode 100644 index 0000000..6fee92f --- /dev/null +++ b/vendor/gipfl/web/src/Form/Validator/AlwaysFailValidator.php @@ -0,0 +1,16 @@ +<?php + +namespace gipfl\Web\Form\Validator; + +class AlwaysFailValidator extends SimpleValidator +{ + public function isValid($value) + { + $message = $this->getSetting('message'); + if ($message) { + $this->addMessage($message); + } + + return false; + } +} diff --git a/vendor/gipfl/web/src/Form/Validator/PhpSessionBasedCsrfTokenValidator.php b/vendor/gipfl/web/src/Form/Validator/PhpSessionBasedCsrfTokenValidator.php new file mode 100644 index 0000000..2f08c6c --- /dev/null +++ b/vendor/gipfl/web/src/Form/Validator/PhpSessionBasedCsrfTokenValidator.php @@ -0,0 +1,34 @@ +<?php + +namespace gipfl\Web\Form\Validator; + +class PhpSessionBasedCsrfTokenValidator extends SimpleValidator +{ + public function isValid($value) + { + if (strpos($value, '|') === false) { + return false; + } + + list($seed, $token) = \explode('|', $value, 2); + + if (! \is_numeric($seed)) { + return false; + } + + if ($token === \hash('sha256', \session_id() . $seed)) { + return true; + } else { + $this->addMessage('An invalid CSRF token has been submitted'); + return false; + } + } + + public static function generateCsrfValue() + { + $seed = \mt_rand(); + $token = \hash('sha256', \session_id() . $seed); + + return \sprintf('%s|%s', $seed, $token); + } +} diff --git a/vendor/gipfl/web/src/Form/Validator/SimpleValidator.php b/vendor/gipfl/web/src/Form/Validator/SimpleValidator.php new file mode 100644 index 0000000..e06a10f --- /dev/null +++ b/vendor/gipfl/web/src/Form/Validator/SimpleValidator.php @@ -0,0 +1,27 @@ +<?php + +namespace gipfl\Web\Form\Validator; + +use ipl\Stdlib\Contract\Validator; +use ipl\Stdlib\Messages; + +abstract class SimpleValidator implements Validator +{ + use Messages; + + protected $settings = []; + + public function __construct(array $settings = []) + { + $this->settings = $settings; + } + + public function getSetting($name, $default = null) + { + if (array_key_exists($name, $this->settings)) { + return $this->settings[$name]; + } else { + return $default; + } + } +} diff --git a/vendor/gipfl/web/src/HtmlHelper.php b/vendor/gipfl/web/src/HtmlHelper.php new file mode 100644 index 0000000..19862f8 --- /dev/null +++ b/vendor/gipfl/web/src/HtmlHelper.php @@ -0,0 +1,29 @@ +<?php + +namespace gipfl\Web; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; + +abstract class HtmlHelper +{ + public static function elementHasClass(BaseHtmlElement $element, $class) + { + return static::classIsSet($element->getAttributes(), $class); + } + + public static function addClassOnce(Attributes $attributes, $class) + { + if (! HtmlHelper::classIsSet($attributes, $class)) { + $attributes->add('class', $class); + } + } + + public static function classIsSet(Attributes $attributes, $class) + { + $classes = $attributes->get('class'); + + return \is_array($classes) && in_array($class, $classes) + || \is_string($classes) && $classes === $class; + } +} diff --git a/vendor/gipfl/web/src/InlineForm.php b/vendor/gipfl/web/src/InlineForm.php new file mode 100644 index 0000000..fd6b301 --- /dev/null +++ b/vendor/gipfl/web/src/InlineForm.php @@ -0,0 +1,10 @@ +<?php + +namespace gipfl\Web; + +class InlineForm extends Form +{ + protected $defaultDecoratorClass = null; + + protected $formCssClasses = ['gipfl-form', 'gipfl-inline-form']; +} diff --git a/vendor/gipfl/web/src/Table/NameValueTable.php b/vendor/gipfl/web/src/Table/NameValueTable.php new file mode 100644 index 0000000..7227de7 --- /dev/null +++ b/vendor/gipfl/web/src/Table/NameValueTable.php @@ -0,0 +1,47 @@ +<?php + +namespace gipfl\Web\Table; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Table; + +class NameValueTable extends Table +{ + protected $defaultAttributes = ['class' => 'gipfl-name-value-table']; + + public static function create($pairs = []) + { + $self = new static; + $self->addNameValuePairs($pairs); + + return $self; + } + + public function createNameValueRow($name, $value) + { + return $this::tr([$this::th($name), $this::wantTd($value)]); + } + + public function addNameValueRow($name, $value) + { + return $this->add($this->createNameValueRow($name, $value)); + } + + public function addNameValuePairs($pairs) + { + foreach ($pairs as $name => $value) { + $this->addNameValueRow($name, $value); + } + + return $this; + } + + protected function wantTd($value) + { + if ($value instanceof BaseHtmlElement && $value->getTag() === 'td') { + return $value; + } else { + return $this::td($value); + } + } +} diff --git a/vendor/gipfl/web/src/Widget/CollapsibleList.php b/vendor/gipfl/web/src/Widget/CollapsibleList.php new file mode 100644 index 0000000..0df8234 --- /dev/null +++ b/vendor/gipfl/web/src/Widget/CollapsibleList.php @@ -0,0 +1,74 @@ +<?php + +namespace gipfl\Web\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use InvalidArgumentException; +use LogicException; +use function count; + +class CollapsibleList extends BaseHtmlElement +{ + protected $tag = 'ul'; + + protected $defaultAttributes = [ + 'class' => 'gipfl-collapsible' + ]; + + protected $defaultListAttributes; + + protected $defaultSectionAttributes; + + protected $items = []; + + public function __construct($items = [], $listAttributes = null) + { + if ($listAttributes !== null) { + $this->defaultListAttributes = $listAttributes; + } + foreach ($items as $title => $item) { + $this->addItem($title, $item); + } + } + + public function addItem($title, $content) + { + if ($this->hasItem($title)) { + throw new LogicException("Cannot add item with title '$title' twice"); + } + $item = Html::tag('li', [ + Html::tag('a', ['href' => '#', 'class' => 'gipfl-collapsible-control'], $title), + $content + ]); + + if (count($this->items) > 0) { + $item->getAttributes()->add('class', 'collapsed'); + } + $this->items[$title] = $item; + } + + public function hasItem($title) + { + return isset($this->items[$title]); + } + + public function getItem($name) + { + if (isset($this->items[$name])) { + return $this->items[$name]; + } + + throw new InvalidArgumentException("There is no '$name' item in this list"); + } + + protected function assemble() + { + if ($this->defaultListAttributes) { + $this->addAttributes($this->defaultListAttributes); + } + foreach ($this->items as $item) { + $this->add($item); + } + } +} diff --git a/vendor/gipfl/web/src/Widget/ConfigDiff.php b/vendor/gipfl/web/src/Widget/ConfigDiff.php new file mode 100644 index 0000000..8ac366f --- /dev/null +++ b/vendor/gipfl/web/src/Widget/ConfigDiff.php @@ -0,0 +1,106 @@ +<?php + +namespace gipfl\Web\Widget; + +use Diff; +use ipl\Html\ValidHtml; +use InvalidArgumentException; + +/** + * @deprecated - please use gipfl\Diff + */ +class ConfigDiff implements ValidHtml +{ + protected $a; + + protected $b; + + protected $diff; + + protected $htmlRenderer = 'SideBySide'; + + protected $knownHtmlRenderers = [ + 'SideBySide', + 'Inline', + ]; + + protected $knownTextRenderers = [ + 'Context', + 'Unified', + ]; + + protected $vendorDir; + + protected function __construct($a, $b) + { + $this->vendorDir = \dirname(\dirname(__DIR__)) . '/vendor'; + require_once $this->vendorDir . '/php-diff/lib/Diff.php'; + + if (empty($a)) { + $this->a = []; + } else { + $this->a = explode("\n", (string) $a); + } + + if (empty($b)) { + $this->b = []; + } else { + $this->b = explode("\n", (string) $b); + } + + $options = [ + 'context' => 5, + // 'ignoreWhitespace' => true, + // 'ignoreCase' => true, + ]; + $this->diff = new Diff($this->a, $this->b, $options); + } + + public function render() + { + return $this->renderHtml(); + } + + /** + * @return string + */ + public function renderHtml() + { + return $this->diff->Render($this->getHtmlRenderer()); + } + + public function setHtmlRenderer($name) + { + if (in_array($name, $this->knownHtmlRenderers)) { + $this->htmlRenderer = $name; + } else { + throw new InvalidArgumentException("There is no known '$name' renderer"); + } + + return $this; + } + + protected function getHtmlRenderer() + { + $filename = sprintf( + '%s/vendor/php-diff/lib/Diff/Renderer/Html/%s.php', + $this->vendorDir, + $this->htmlRenderer + ); + require_once($filename); + + $class = 'Diff_Renderer_Html_' . $this->htmlRenderer; + + return new $class(); + } + + public function __toString() + { + return $this->renderHtml(); + } + + public static function create($a, $b) + { + return new static($a, $b); + } +} diff --git a/vendor/gipfl/web/src/Widget/Hint.php b/vendor/gipfl/web/src/Widget/Hint.php new file mode 100644 index 0000000..785d9e4 --- /dev/null +++ b/vendor/gipfl/web/src/Widget/Hint.php @@ -0,0 +1,45 @@ +<?php + +namespace gipfl\Web\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class Hint extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = [ + 'class' => 'gipfl-widget-hint' + ]; + + public function __construct($message, $class = 'ok', ...$params) + { + $this->addAttributes(['class' => $class]); + if (empty($params)) { + $this->setContent($message); + } else { + $this->setContent(Html::sprintf($message, ...$params)); + } + } + + public static function ok($message, ...$params) + { + return new static($message, 'ok', ...$params); + } + + public static function info($message, ...$params) + { + return new static($message, 'info', ...$params); + } + + public static function warning($message, ...$params) + { + return new static($message, 'warning', ...$params); + } + + public static function error($message, ...$params) + { + return new static($message, 'error', ...$params); + } +} diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff.php new file mode 100644 index 0000000..d1eb9da --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff.php @@ -0,0 +1,179 @@ +<?php +/** + * Diff + * + * A comprehensive library for generating differences between two strings + * in multiple formats (unified, side by side HTML etc) + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Diff + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +class Diff +{ + /** + * @var array The "old" sequence to use as the basis for the comparison. + */ + private $a = null; + + /** + * @var array The "new" sequence to generate the changes for. + */ + private $b = null; + + /** + * @var array Array containing the generated opcodes for the differences between the two items. + */ + private $groupedCodes = null; + + /** + * @var array Associative array of the default options available for the diff class and their default value. + */ + private $defaultOptions = array( + 'context' => 3, + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ); + + /** + * @var array Array of the options that have been applied for generating the diff. + */ + private $options = array(); + + /** + * The constructor. + * + * @param array $a Array containing the lines of the first string to compare. + * @param array $b Array containing the lines for the second string to compare. + */ + public function __construct($a, $b, $options=array()) + { + $this->a = $a; + $this->b = $b; + + if (is_array($options)) + $this->options = array_merge($this->defaultOptions, $options); + else + $this->options = $this->defaultOptions; + } + + /** + * Render a diff using the supplied rendering class and return it. + * + * @param object $renderer An instance of the rendering object to use for generating the diff. + * @return mixed The generated diff. Exact return value depends on the rendered. + */ + public function render(Diff_Renderer_Abstract $renderer) + { + $renderer->diff = $this; + return $renderer->render(); + } + + /** + * Get a range of lines from $start to $end from the first comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getA($start=0, $end=null) + { + if($start == 0 && $end === null) { + return $this->a; + } + + if($end === null) { + $length = 1; + } + else { + $length = $end - $start; + } + + return array_slice($this->a, $start, $length); + + } + + /** + * Get a range of lines from $start to $end from the second comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getB($start=0, $end=null) + { + if($start == 0 && $end === null) { + return $this->b; + } + + if($end === null) { + $length = 1; + } + else { + $length = $end - $start; + } + + return array_slice($this->b, $start, $length); + } + + /** + * Generate a list of the compiled and grouped opcodes for the differences between the + * two strings. Generally called by the renderer, this class instantiates the sequence + * matcher and performs the actual diff generation and return an array of the opcodes + * for it. Once generated, the results are cached in the diff class instance. + * + * @return array Array of the grouped opcodes for the generated diff. + */ + public function getGroupedOpcodes() + { + if(!is_null($this->groupedCodes)) { + return $this->groupedCodes; + } + + require_once dirname(__FILE__).'/Diff/SequenceMatcher.php'; + $sequenceMatcher = new Diff_SequenceMatcher($this->a, $this->b, null, $this->options); + $this->groupedCodes = $sequenceMatcher->getGroupedOpcodes($this->options['context']); + return $this->groupedCodes; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Abstract.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Abstract.php new file mode 100644 index 0000000..f63c3e7 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Abstract.php @@ -0,0 +1,82 @@ +<?php +/** + * Abstract class for diff renderers in PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +abstract class Diff_Renderer_Abstract +{ + /** + * @var object Instance of the diff class that this renderer is generating the rendered diff for. + */ + public $diff; + + /** + * @var array Array of the default options that apply to this renderer. + */ + protected $defaultOptions = array(); + + /** + * @var array Array containing the user applied and merged default options for the renderer. + */ + protected $options = array(); + + /** + * The constructor. Instantiates the rendering engine and if options are passed, + * sets the options for the renderer. + * + * @param array $options Optionally, an array of the options for the renderer. + */ + public function __construct(array $options = array()) + { + $this->setOptions($options); + } + + /** + * Set the options of the renderer to those supplied in the passed in array. + * Options are merged with the default to ensure that there aren't any missing + * options. + * + * @param array $options Array of options to set. + */ + public function setOptions(array $options) + { + $this->options = array_merge($this->defaultOptions, $options); + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Array.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Array.php new file mode 100644 index 0000000..2fe9625 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Array.php @@ -0,0 +1,230 @@ +<?php +/** + * Base renderer for rendering HTML based diffs for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract +{ + /** + * @var array Array of the default options that apply to this renderer. + */ + protected $defaultOptions = array( + 'tabSize' => 4 + ); + + /** + * Render and return an array structure suitable for generating HTML + * based differences. Generally called by subclasses that generate a + * HTML based diff and return an array of the changes to show in the diff. + * + * @return array An array of the generated chances, suitable for presentation in HTML. + */ + public function render() + { + // As we'll be modifying a & b to include our change markers, + // we need to get the contents and store them here. That way + // we're not going to destroy the original data + $a = $this->diff->getA(); + $b = $this->diff->getB(); + + $changes = array(); + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $blocks = array(); + $lastTag = null; + $lastBlock = 0; + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + + if($tag == 'replace' && $i2 - $i1 == $j2 - $j1) { + for($i = 0; $i < ($i2 - $i1); ++$i) { + $fromLine = $a[$i1 + $i]; + $toLine = $b[$j1 + $i]; + + list($start, $end) = $this->getChangeExtent($fromLine, $toLine); + if($start != 0 || $end != 0) { + $last = $end + strlen($fromLine); + $fromLine = substr_replace($fromLine, "\0", $start, 0); + $fromLine = substr_replace($fromLine, "\1", $last + 1, 0); + $last = $end + strlen($toLine); + $toLine = substr_replace($toLine, "\0", $start, 0); + $toLine = substr_replace($toLine, "\1", $last + 1, 0); + $a[$i1 + $i] = $fromLine; + $b[$j1 + $i] = $toLine; + } + } + } + + if($tag != $lastTag) { + $blocks[] = array( + 'tag' => $tag, + 'base' => array( + 'offset' => $i1, + 'lines' => array() + ), + 'changed' => array( + 'offset' => $j1, + 'lines' => array() + ) + ); + $lastBlock = count($blocks)-1; + } + + $lastTag = $tag; + + if($tag == 'equal') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines); + $lines = array_slice($b, $j1, ($j2 - $j1)); + $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines); + } + else { + if($tag == 'replace' || $tag == 'delete') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('<del>', '</del>'), $lines); + $blocks[$lastBlock]['base']['lines'] += $lines; + } + + if($tag == 'replace' || $tag == 'insert') { + $lines = array_slice($b, $j1, ($j2 - $j1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('<ins>', '</ins>'), $lines); + $blocks[$lastBlock]['changed']['lines'] += $lines; + } + } + } + $changes[] = $blocks; + } + return $changes; + } + + /** + * Given two strings, determine where the changes in the two strings + * begin, and where the changes in the two strings end. + * + * @param string $fromLine The first string. + * @param string $toLine The second string. + * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) + */ + private function getChangeExtent($fromLine, $toLine) + { + $start = 0; + $limit = min(strlen($fromLine), strlen($toLine)); + while($start < $limit && $fromLine{$start} == $toLine{$start}) { + ++$start; + } + $end = -1; + $limit = $limit - $start; + while(-$end <= $limit && substr($fromLine, $end, 1) == substr($toLine, $end, 1)) { + --$end; + } + return array( + $start, + $end + 1 + ); + } + + /** + * Format a series of lines suitable for output in a HTML rendered diff. + * This involves replacing tab characters with spaces, making the HTML safe + * for output, ensuring that double spaces are replaced with etc. + * + * @param array $lines Array of lines to format. + * @return array Array of the formatted lines. + */ + protected function formatLines($lines) + { + $lines = array_map(array($this, 'ExpandTabs'), $lines); + $lines = array_map(array($this, 'HtmlSafe'), $lines); + foreach($lines as &$line) { + $line = preg_replace_callback('# ( +)|^ #', array($this, 'fixSpaces'), $line); + } + return $lines; + } + + /** + * Replace a string containing spaces with a HTML representation using . + * + * @param string[] $matches Array with preg matches. + * @return string The HTML representation of the string. + */ + private function fixSpaces(array $matches) + { + $count = 0; + + if (count($matches) > 1) { + $spaces = $matches[1]; + $count = strlen($spaces); + } + + if ($count == 0) { + return ''; + } + + $div = floor($count / 2); + $mod = $count % 2; + return str_repeat(' ', $div).str_repeat(' ', $mod); + } + + /** + * Replace tabs in a single line with a number of spaces as defined by the tabSize option. + * + * @param string $line The containing tabs to convert. + * @return string The line with the tabs converted to spaces. + */ + private function expandTabs($line) + { + return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line); + } + + /** + * Make a string containing HTML safe for output on a page. + * + * @param string $string The string. + * @return string The string with the HTML characters replaced by entities. + */ + private function htmlSafe($string) + { + return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8'); + } +} diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php new file mode 100644 index 0000000..a37fec6 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php @@ -0,0 +1,143 @@ +<?php +/** + * Inline HTML diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/Array.php'; + +class Diff_Renderer_Html_Inline extends Diff_Renderer_Html_Array +{ + /** + * Render a and return diff with changes between the two sequences + * displayed inline (under each other) + * + * @return string The generated inline diff. + */ + public function render() + { + $changes = parent::render(); + $html = ''; + if(empty($changes)) { + return $html; + } + + $html .= '<table class="Differences DifferencesInline">'; + $html .= '<thead>'; + $html .= '<tr>'; + $html .= '<th>Old</th>'; + $html .= '<th>New</th>'; + $html .= '<th>Differences</th>'; + $html .= '</tr>'; + $html .= '</thead>'; + foreach($changes as $i => $blocks) { + // If this is a separate block, we're condensing code so output ..., + // indicating a significant portion of the code has been collapsed as + // it is the same + if($i > 0) { + $html .= '<tbody class="Skipped">'; + $html .= '<th>…</th>'; + $html .= '<th>…</th>'; + $html .= '<td> </td>'; + $html .= '</tbody>'; + } + + foreach($blocks as $change) { + $html .= '<tbody class="Change'.ucfirst($change['tag']).'">'; + // Equal changes should be shown on both sides of the diff + if($change['tag'] == 'equal') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Left">'.$line.'</td>'; + $html .= '</tr>'; + } + } + // Added lines only on the right side + else if($change['tag'] == 'insert') { + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><ins>'.$line.'</ins> </td>'; + $html .= '</tr>'; + } + } + // Show deleted lines only on the left side + else if($change['tag'] == 'delete') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"><del>'.$line.'</del> </td>'; + $html .= '</tr>'; + } + } + // Show modified lines on both sides + else if($change['tag'] == 'replace') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"><span>'.$line.'</span></td>'; + $html .= '</tr>'; + } + + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><span>'.$line.'</span></td>'; + $html .= '</tr>'; + } + } + $html .= '</tbody>'; + } + } + $html .= '</table>'; + return $html; + } +} diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php new file mode 100644 index 0000000..307af1c --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php @@ -0,0 +1,163 @@ +<?php +/** + * Side by Side HTML diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/Array.php'; + +class Diff_Renderer_Html_SideBySide extends Diff_Renderer_Html_Array +{ + /** + * Render a and return diff with changes between the two sequences + * displayed side by side. + * + * @return string The generated side by side diff. + */ + public function render() + { + $changes = parent::render(); + + $html = ''; + if(empty($changes)) { + return $html; + } + + $html .= '<table class="Differences DifferencesSideBySide">'; + $html .= '<thead>'; + $html .= '<tr>'; + $html .= '<th colspan="2">Old Version</th>'; + $html .= '<th colspan="2">New Version</th>'; + $html .= '</tr>'; + $html .= '</thead>'; + foreach($changes as $i => $blocks) { + if($i > 0) { + $html .= '<tbody class="Skipped">'; + $html .= '<th>…</th><td> </td>'; + $html .= '<th>…</th><td> </td>'; + $html .= '</tbody>'; + } + + foreach($blocks as $change) { + $html .= '<tbody class="Change'.ucfirst($change['tag']).'">'; + // Equal changes should be shown on both sides of the diff + if($change['tag'] == 'equal') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><span>'.$line.'</span> </span></td>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><span>'.$line.'</span> </span></td>'; + $html .= '</tr>'; + } + } + // Added lines only on the right side + else if($change['tag'] == 'insert') { + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"> </td>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><ins>'.$line.'</ins> </td>'; + $html .= '</tr>'; + } + } + // Show deleted lines only on the left side + else if($change['tag'] == 'delete') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><del>'.$line.'</del> </td>'; + $html .= '<th> </th>'; + $html .= '<td class="Right"> </td>'; + $html .= '</tr>'; + } + } + // Show modified lines on both sides + else if($change['tag'] == 'replace') { + if(count($change['base']['lines']) >= count($change['changed']['lines'])) { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><span>'.$line.'</span> </td>'; + if(!isset($change['changed']['lines'][$no])) { + $toLine = ' '; + $changedLine = ' '; + } + else { + $toLine = $change['base']['offset'] + $no + 1; + $changedLine = '<span>'.$change['changed']['lines'][$no].'</span>'; + } + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right">'.$changedLine.'</td>'; + $html .= '</tr>'; + } + } + else { + foreach($change['changed']['lines'] as $no => $changedLine) { + if(!isset($change['base']['lines'][$no])) { + $fromLine = ' '; + $line = ' '; + } + else { + $fromLine = $change['base']['offset'] + $no + 1; + $line = '<span>'.$change['base']['lines'][$no].'</span>'; + } + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><span>'.$line.'</span> </td>'; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right">'.$changedLine.'</td>'; + $html .= '</tr>'; + } + } + } + $html .= '</tbody>'; + } + } + $html .= '</table>'; + return $html; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Context.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Context.php new file mode 100644 index 0000000..1200b01 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Context.php @@ -0,0 +1,128 @@ +<?php +/** + * Context diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Text_Context extends Diff_Renderer_Abstract +{ + /** + * @var array Array of the different opcode tags and how they map to the context diff equivalent. + */ + private $tagMap = array( + 'insert' => '+', + 'delete' => '-', + 'replace' => '!', + 'equal' => ' ' + ); + + /** + * Render and return a context formatted (old school!) diff file. + * + * @return string The generated context diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $diff .= "***************\n"; + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if($i2 - $i1 >= 2) { + $diff .= '*** '.($group[0][1] + 1).','.$i2." ****\n"; + } + else { + $diff .= '*** '.$i2." ****\n"; + } + + if($j2 - $j1 >= 2) { + $separator = '--- '.($j1 + 1).','.$j2." ----\n"; + } + else { + $separator = '--- '.$j2." ----\n"; + } + + $hasVisible = false; + foreach($group as $code) { + if($code[0] == 'replace' || $code[0] == 'delete') { + $hasVisible = true; + break; + } + } + + if($hasVisible) { + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'insert') { + continue; + } + $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetA($i1, $i2))."\n"; + } + } + + $hasVisible = false; + foreach($group as $code) { + if($code[0] == 'replace' || $code[0] == 'insert') { + $hasVisible = true; + break; + } + } + + $diff .= $separator; + + if($hasVisible) { + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'delete') { + continue; + } + $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetB($j1, $j2))."\n"; + } + } + } + return $diff; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php new file mode 100644 index 0000000..e94d951 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php @@ -0,0 +1,87 @@ +<?php +/** + * Unified diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Text_Unified extends Diff_Renderer_Abstract +{ + /** + * Render and return a unified diff. + * + * @return string The unified diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if($i1 == 0 && $i2 == 0) { + $i1 = -1; + $i2 = -1; + } + + $diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@\n"; + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'equal') { + $diff .= ' '.implode("\n ", $this->diff->GetA($i1, $i2))."\n"; + } + else { + if($tag == 'replace' || $tag == 'delete') { + $diff .= '-'.implode("\n-", $this->diff->GetA($i1, $i2))."\n"; + } + + if($tag == 'replace' || $tag == 'insert') { + $diff .= '+'.implode("\n+", $this->diff->GetB($j1, $j2))."\n"; + } + } + } + } + return $diff; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/SequenceMatcher.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/SequenceMatcher.php new file mode 100644 index 0000000..a289e39 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/SequenceMatcher.php @@ -0,0 +1,742 @@ +<?php +/** + * Sequence matcher for Diff + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Diff + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +class Diff_SequenceMatcher +{ + /** + * @var string|array Either a string or an array containing a callback function to determine if a line is "junk" or not. + */ + private $junkCallback = null; + + /** + * @var array The first sequence to compare against. + */ + private $a = array(); + + /** + * @var array The second sequence. + */ + private $b = array(); + + /** + * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. + */ + private $junkDict = array(); + + /** + * @var array Array of indices that do not contain junk elements. + */ + private $b2j = array(); + + private $options = array(); + + private $defaultOptions = array( + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ); + + /** + * The constructor. With the sequences being passed, they'll be set for the + * sequence matcher and it will perform a basic cleanup & calculate junk + * elements. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + * @param string|array $junkCallback Either an array or string that references a callback function (if there is one) to determine 'junk' characters. + */ + public function __construct($a, $b, $junkCallback=null, $options) + { + $this->a = array(); + $this->b = array(); + $this->junkCallback = $junkCallback; + $this->setOptions($options); + $this->setSequences($a, $b); + } + + public function setOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + } + + /** + * Set the first and second sequences to use with the sequence matcher. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + */ + public function setSequences($a, $b) + { + $this->setSeq1($a); + $this->setSeq2($b); + } + + /** + * Set the first sequence ($a) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $a The sequence to set as the first sequence. + */ + public function setSeq1($a) + { + if(!is_array($a)) { + $a = str_split($a); + } + if($a == $this->a) { + return; + } + + $this->a= $a; + $this->matchingBlocks = null; + $this->opCodes = null; + } + + /** + * Set the second sequence ($b) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $b The sequence to set as the second sequence. + */ + public function setSeq2($b) + { + if(!is_array($b)) { + $b = str_split($b); + } + if($b == $this->b) { + return; + } + + $this->b = $b; + $this->matchingBlocks = null; + $this->opCodes = null; + $this->fullBCount = null; + $this->chainB(); + } + + /** + * Generate the internal arrays containing the list of junk and non-junk + * characters for the second ($b) sequence. + */ + private function chainB() + { + $length = count ($this->b); + $this->b2j = array(); + $popularDict = array(); + + for($i = 0; $i < $length; ++$i) { + $char = $this->b[$i]; + if(isset($this->b2j[$char])) { + if($length >= 200 && count($this->b2j[$char]) * 100 > $length) { + $popularDict[$char] = 1; + unset($this->b2j[$char]); + } + else { + $this->b2j[$char][] = $i; + } + } + else { + $this->b2j[$char] = array( + $i + ); + } + } + + // Remove leftovers + foreach(array_keys($popularDict) as $char) { + unset($this->b2j[$char]); + } + + $this->junkDict = array(); + if(is_callable($this->junkCallback)) { + foreach(array_keys($popularDict) as $char) { + if(call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($popularDict[$char]); + } + } + + foreach(array_keys($this->b2j) as $char) { + if(call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($this->b2j[$char]); + } + } + } + } + + /** + * Checks if a particular character is in the junk dictionary + * for the list of junk characters. + * + * @return boolean $b True if the character is considered junk. False if not. + */ + private function isBJunk($b) + { + if(isset($this->juncDict[$b])) { + return true; + } + + return false; + } + + /** + * Find the longest matching block in the two sequences, as defined by the + * lower and upper constraints for each sequence. (for the first sequence, + * $alo - $ahi and for the second sequence, $blo - $bhi) + * + * Essentially, of all of the maximal matching blocks, return the one that + * startest earliest in $a, and all of those maximal matching blocks that + * start earliest in $a, return the one that starts earliest in $b. + * + * If the junk callback is defined, do the above but with the restriction + * that the junk element appears in the block. Extend it as far as possible + * by matching only junk elements in both $a and $b. + * + * @param int $alo The lower constraint for the first sequence. + * @param int $ahi The upper constraint for the first sequence. + * @param int $blo The lower constraint for the second sequence. + * @param int $bhi The upper constraint for the second sequence. + * @return array Array containing the longest match that includes the starting position in $a, start in $b and the length/size. + */ + public function findLongestMatch($alo, $ahi, $blo, $bhi) + { + $a = $this->a; + $b = $this->b; + + $bestI = $alo; + $bestJ = $blo; + $bestSize = 0; + + $j2Len = array(); + $nothing = array(); + + for($i = $alo; $i < $ahi; ++$i) { + $newJ2Len = array(); + $jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing); + foreach($jDict as $jKey => $j) { + if($j < $blo) { + continue; + } + else if($j >= $bhi) { + break; + } + + $k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1; + $newJ2Len[$j] = $k; + if($k > $bestSize) { + $bestI = $i - $k + 1; + $bestJ = $j - $k + 1; + $bestSize = $k; + } + } + + $j2Len = $newJ2Len; + } + + while($bestI > $alo && $bestJ > $blo && !$this->isBJunk($b[$bestJ - 1]) && + !$this->linesAreDifferent($bestI - 1, $bestJ - 1)) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while($bestI + $bestSize < $ahi && ($bestJ + $bestSize) < $bhi && + !$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { + ++$bestSize; + } + + while($bestI > $alo && $bestJ > $blo && $this->isBJunk($b[$bestJ - 1]) && + !$this->isLineDifferent($bestI - 1, $bestJ - 1)) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while($bestI + $bestSize < $ahi && $bestJ + $bestSize < $bhi && + $this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { + ++$bestSize; + } + + return array( + $bestI, + $bestJ, + $bestSize + ); + } + + /** + * Check if the two lines at the given indexes are different or not. + * + * @param int $aIndex Line number to check against in a. + * @param int $bIndex Line number to check against in b. + * @return boolean True if the lines are different and false if not. + */ + public function linesAreDifferent($aIndex, $bIndex) + { + $lineA = $this->a[$aIndex]; + $lineB = $this->b[$bIndex]; + + if($this->options['ignoreWhitespace']) { + $replace = array("\t", ' '); + $lineA = str_replace($replace, '', $lineA); + $lineB = str_replace($replace, '', $lineB); + } + + if($this->options['ignoreCase']) { + $lineA = strtolower($lineA); + $lineB = strtolower($lineB); + } + + if($lineA != $lineB) { + return true; + } + + return false; + } + + /** + * Return a nested set of arrays for all of the matching sub-sequences + * in the strings $a and $b. + * + * Each block contains the lower constraint of the block in $a, the lower + * constraint of the block in $b and finally the number of lines that the + * block continues for. + * + * @return array Nested array of the matching blocks, as described by the function. + */ + public function getMatchingBlocks() + { + if(!empty($this->matchingBlocks)) { + return $this->matchingBlocks; + } + + $aLength = count($this->a); + $bLength = count($this->b); + + $queue = array( + array( + 0, + $aLength, + 0, + $bLength + ) + ); + + $matchingBlocks = array(); + while(!empty($queue)) { + list($alo, $ahi, $blo, $bhi) = array_pop($queue); + $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi); + list($i, $j, $k) = $x; + if($k) { + $matchingBlocks[] = $x; + if($alo < $i && $blo < $j) { + $queue[] = array( + $alo, + $i, + $blo, + $j + ); + } + + if($i + $k < $ahi && $j + $k < $bhi) { + $queue[] = array( + $i + $k, + $ahi, + $j + $k, + $bhi + ); + } + } + } + + usort($matchingBlocks, array($this, 'tupleSort')); + + $i1 = 0; + $j1 = 0; + $k1 = 0; + $nonAdjacent = array(); + foreach($matchingBlocks as $block) { + list($i2, $j2, $k2) = $block; + if($i1 + $k1 == $i2 && $j1 + $k1 == $j2) { + $k1 += $k2; + } + else { + if($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $i1 = $i2; + $j1 = $j2; + $k1 = $k2; + } + } + + if($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $nonAdjacent[] = array( + $aLength, + $bLength, + 0 + ); + + $this->matchingBlocks = $nonAdjacent; + return $this->matchingBlocks; + } + + /** + * Return a list of all of the opcodes for the differences between the + * two strings. + * + * The nested array returned contains an array describing the opcode + * which includes: + * 0 - The type of tag (as described below) for the opcode. + * 1 - The beginning line in the first sequence. + * 2 - The end line in the first sequence. + * 3 - The beginning line in the second sequence. + * 4 - The end line in the second sequence. + * + * The different types of tags include: + * replace - The string from $i1 to $i2 in $a should be replaced by + * the string in $b from $j1 to $j2. + * delete - The string in $a from $i1 to $j2 should be deleted. + * insert - The string in $b from $j1 to $j2 should be inserted at + * $i1 in $a. + * equal - The two strings with the specified ranges are equal. + * + * @return array Array of the opcodes describing the differences between the strings. + */ + public function getOpCodes() + { + if(!empty($this->opCodes)) { + return $this->opCodes; + } + + $i = 0; + $j = 0; + $this->opCodes = array(); + + $blocks = $this->getMatchingBlocks(); + foreach($blocks as $block) { + list($ai, $bj, $size) = $block; + $tag = ''; + if($i < $ai && $j < $bj) { + $tag = 'replace'; + } + else if($i < $ai) { + $tag = 'delete'; + } + else if($j < $bj) { + $tag = 'insert'; + } + + if($tag) { + $this->opCodes[] = array( + $tag, + $i, + $ai, + $j, + $bj + ); + } + + $i = $ai + $size; + $j = $bj + $size; + + if($size) { + $this->opCodes[] = array( + 'equal', + $ai, + $i, + $bj, + $j + ); + } + } + return $this->opCodes; + } + + /** + * Return a series of nested arrays containing different groups of generated + * opcodes for the differences between the strings with up to $context lines + * of surrounding content. + * + * Essentially what happens here is any big equal blocks of strings are stripped + * out, the smaller subsets of changes are then arranged in to their groups. + * This means that the sequence matcher and diffs do not need to include the full + * content of the different files but can still provide context as to where the + * changes are. + * + * @param int $context The number of lines of context to provide around the groups. + * @return array Nested array of all of the grouped opcodes. + */ + public function getGroupedOpcodes($context=3) + { + $opCodes = $this->getOpCodes(); + if(empty($opCodes)) { + $opCodes = array( + array( + 'equal', + 0, + 1, + 0, + 1 + ) + ); + } + + if($opCodes[0][0] == 'equal') { + $opCodes[0] = array( + $opCodes[0][0], + max($opCodes[0][1], $opCodes[0][2] - $context), + $opCodes[0][2], + max($opCodes[0][3], $opCodes[0][4] - $context), + $opCodes[0][4] + ); + } + + $lastItem = count($opCodes) - 1; + if($opCodes[$lastItem][0] == 'equal') { + list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem]; + $opCodes[$lastItem] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + } + + $maxRange = $context * 2; + $groups = array(); + $group = array(); + foreach($opCodes as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'equal' && $i2 - $i1 > $maxRange) { + $group[] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + $groups[] = $group; + $group = array(); + $i1 = max($i1, $i2 - $context); + $j1 = max($j1, $j2 - $context); + } + $group[] = array( + $tag, + $i1, + $i2, + $j1, + $j2 + ); + } + + if(!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal')) { + $groups[] = $group; + } + + return $groups; + } + + /** + * Return a measure of the similarity between the two sequences. + * This will be a float value between 0 and 1. + * + * Out of all of the ratio calculation functions, this is the most + * expensive to call if getMatchingBlocks or getOpCodes is yet to be + * called. The other calculation methods (quickRatio and realquickRatio) + * can be used to perform quicker calculations but may be less accurate. + * + * The ratio is calculated as (2 * number of matches) / total number of + * elements in both sequences. + * + * @return float The calculated ratio. + */ + public function Ratio() + { + $matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0); + return $this->calculateRatio($matches, count ($this->a) + count ($this->b)); + } + + /** + * Helper function to calculate the number of matches for Ratio(). + * + * @param int $sum The running total for the number of matches. + * @param array $triple Array containing the matching block triple to add to the running total. + * @return int The new running total for the number of matches. + */ + private function ratioReduce($sum, $triple) + { + return $sum + ($triple[count($triple) - 1]); + } + + /** + * Quickly return an upper bound ratio for the similarity of the strings. + * This is quicker to compute than Ratio(). + * + * @return float The calculated ratio. + */ + private function quickRatio() + { + if($this->fullBCount === null) { + $this->fullBCount = array(); + $bLength = count ($b); + for($i = 0; $i < $bLength; ++$i) { + $char = $this->b[$i]; + $this->fullBCount[$char] = $this->arrayGetDefault($this->fullBCount, $char, 0) + 1; + } + } + + $avail = array(); + $matches = 0; + $aLength = count ($this->a); + for($i = 0; $i < $aLength; ++$i) { + $char = $this->a[$i]; + if(isset($avail[$char])) { + $numb = $avail[$char]; + } + else { + $numb = $this->arrayGetDefault($this->fullBCount, $char, 0); + } + $avail[$char] = $numb - 1; + if($numb > 0) { + ++$matches; + } + } + + $this->calculateRatio($matches, count ($this->a) + count ($this->b)); + } + + /** + * Return an upper bound ratio really quickly for the similarity of the strings. + * This is quicker to compute than Ratio() and quickRatio(). + * + * @return float The calculated ratio. + */ + private function realquickRatio() + { + $aLength = count ($this->a); + $bLength = count ($this->b); + + return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); + } + + /** + * Helper function for calculating the ratio to measure similarity for the strings. + * The ratio is defined as being 2 * (number of matches / total length) + * + * @param int $matches The number of matches in the two strings. + * @param int $length The length of the two strings. + * @return float The calculated ratio. + */ + private function calculateRatio($matches, $length=0) + { + if($length) { + return 2 * ($matches / $length); + } + else { + return 1; + } + } + + /** + * Helper function that provides the ability to return the value for a key + * in an array of it exists, or if it doesn't then return a default value. + * Essentially cleaner than doing a series of if(isset()) {} else {} calls. + * + * @param array $array The array to search. + * @param string $key The key to check that exists. + * @param mixed $default The value to return as the default value if the key doesn't exist. + * @return mixed The value from the array if the key exists or otherwise the default. + */ + private function arrayGetDefault($array, $key, $default) + { + if(isset($array[$key])) { + return $array[$key]; + } + else { + return $default; + } + } + + /** + * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks + * + * @param array $a First array to compare. + * @param array $b Second array to compare. + * @return int -1, 0 or 1, as expected by the usort function. + */ + private function tupleSort($a, $b) + { + $max = max(count($a), count($b)); + for($i = 0; $i < $max; ++$i) { + if($a[$i] < $b[$i]) { + return -1; + } + else if($a[$i] > $b[$i]) { + return 1; + } + } + + if(count($a) == $count($b)) { + return 0; + } + else if(count($a) < count($b)) { + return -1; + } + else { + return 1; + } + } +} diff --git a/vendor/gipfl/zfdb/composer.json b/vendor/gipfl/zfdb/composer.json new file mode 100644 index 0000000..742235a --- /dev/null +++ b/vendor/gipfl/zfdb/composer.json @@ -0,0 +1,16 @@ +{ + "name": "gipfl/zfdb", + "description": "Zend_Db from Zend Framework 1. For compatibility reasons only", + "type": "library", + "require": { + "php": ">=5.4" + }, + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\ZfDb\\": "src" + } + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Adapter.php b/vendor/gipfl/zfdb/src/Adapter/Adapter.php new file mode 100644 index 0000000..a3cad5d --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Adapter.php @@ -0,0 +1,1267 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Expr; +use gipfl\ZfDb\Profiler; +use gipfl\ZfDb\Profiler\ProfilerException; +use gipfl\ZfDb\Select; +use gipfl\ZfDb\Statement; +use gipfl\ZfDb\Statement\StatementInterface; + +/** + * @see Zend_Db + */ + +/** + * @see Select + */ + +/** + * Class for connecting to SQL databases and performing common operations. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +abstract class Adapter +{ + + /** + * User-provided configuration + * + * @var array + */ + protected $_config = array(); + + /** + * Fetch mode + * + * @var integer + */ + protected $_fetchMode = Db::FETCH_ASSOC; + + /** + * Query profiler object, of type Zend_Db_Profiler + * or a subclass of that. + * + * @var Profiler + */ + protected $_profiler; + + /** + * Default class name for a DB statement. + * + * @var string + */ + protected $_defaultStmtClass = Statement::class; + + /** + * Default class name for the profiler object. + * + * @var string + */ + protected $_defaultProfilerClass = Profiler::class; + + /** + * Database connection + * + * @var object|resource|null + */ + protected $_connection = null; + + /** + * Specifies the case of column names retrieved in queries + * Options + * Zend_Db::CASE_NATURAL (default) + * Zend_Db::CASE_LOWER + * Zend_Db::CASE_UPPER + * + * @var integer + */ + protected $_caseFolding = Db::CASE_NATURAL; + + /** + * Specifies whether the adapter automatically quotes identifiers. + * If true, most SQL generated by Zend_Db classes applies + * identifier quoting automatically. + * If false, developer must quote identifiers themselves + * by calling quoteIdentifier(). + * + * @var bool + */ + protected $_autoQuoteIdentifiers = true; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE + ); + + /** Weither or not that object can get serialized + * + * @var bool + */ + protected $_allowSerialization = true; + + /** + * Weither or not the database should be reconnected + * to that adapter when waking up + * + * @var bool + */ + protected $_autoReconnectOnUnserialize = false; + + /** + * Constructor. + * + * $config is an array of key/value pairs or an instance of Zend_Config + * containing configuration options. These options are common to most adapters: + * + * dbname => (string) The name of the database to user + * username => (string) Connect to the database as this username. + * password => (string) Password associated with the username. + * host => (string) What host to connect to, defaults to localhost + * + * Some options are used on a case-by-case basis by adapters: + * + * port => (string) The port of the database + * persistent => (boolean) Whether to use a persistent connection or not, defaults to false + * protocol => (string) The network protocol, defaults to TCPIP + * caseFolding => (int) style of case-alteration used for identifiers + * socket => (string) The socket or named pipe that should be used + * + * @param array|Zend_Config $config An array or instance of Zend_Config having configuration data + * @throws AdapterException + */ + public function __construct($config) + { + /* + * Verify that adapter parameters are in an array. + */ + if (!is_array($config)) { + /* + * Convert Zend_Config argument to a plain array. + */ + if ($config instanceof Zend_Config) { + $config = $config->toArray(); + } else { + /** + * @see AdapterException + */ + throw new AdapterException('Adapter parameters must be in an array or a Zend_Config object'); + } + } + + $this->_checkRequiredOptions($config); + + $options = array( + Db::CASE_FOLDING => $this->_caseFolding, + Db::AUTO_QUOTE_IDENTIFIERS => $this->_autoQuoteIdentifiers, + Db::FETCH_MODE => $this->_fetchMode, + ); + $driverOptions = array(); + + /* + * normalize the config and merge it with the defaults + */ + if (array_key_exists('options', $config)) { + // can't use array_merge() because keys might be integers + foreach ((array) $config['options'] as $key => $value) { + $options[$key] = $value; + } + } + if (array_key_exists('driver_options', $config)) { + if (!empty($config['driver_options'])) { + // can't use array_merge() because keys might be integers + foreach ((array) $config['driver_options'] as $key => $value) { + $driverOptions[$key] = $value; + } + } + } + + if (!isset($config['charset'])) { + $config['charset'] = null; + } + + if (!isset($config['persistent'])) { + $config['persistent'] = false; + } + + $this->_config = array_merge($this->_config, $config); + $this->_config['options'] = $options; + $this->_config['driver_options'] = $driverOptions; + + + // obtain the case setting, if there is one + if (array_key_exists(Db::CASE_FOLDING, $options)) { + $case = (int) $options[Db::CASE_FOLDING]; + switch ($case) { + case Db::CASE_LOWER: + case Db::CASE_UPPER: + case Db::CASE_NATURAL: + $this->_caseFolding = $case; + break; + default: + /** @see AdapterException */ + throw new AdapterException('Case must be one of the following constants: ' + . 'Zend_Db::CASE_NATURAL, Zend_Db::CASE_LOWER, Zend_Db::CASE_UPPER'); + } + } + + if (array_key_exists(Db::FETCH_MODE, $options)) { + if (is_string($options[Db::FETCH_MODE])) { + $constant = 'Db::FETCH_' . strtoupper($options[Db::FETCH_MODE]); + if (defined($constant)) { + $options[Db::FETCH_MODE] = constant($constant); + } + } + $this->setFetchMode((int) $options[Db::FETCH_MODE]); + } + + // obtain quoting property if there is one + if (array_key_exists(Db::AUTO_QUOTE_IDENTIFIERS, $options)) { + $this->_autoQuoteIdentifiers = (bool) $options[Db::AUTO_QUOTE_IDENTIFIERS]; + } + + // obtain allow serialization property if there is one + if (array_key_exists(Db::ALLOW_SERIALIZATION, $options)) { + $this->_allowSerialization = (bool) $options[Db::ALLOW_SERIALIZATION]; + } + + // obtain auto reconnect on unserialize property if there is one + if (array_key_exists(Db::AUTO_RECONNECT_ON_UNSERIALIZE, $options)) { + $this->_autoReconnectOnUnserialize = (bool) $options[Db::AUTO_RECONNECT_ON_UNSERIALIZE]; + } + + // create a profiler object + $profiler = false; + if (array_key_exists(Db::PROFILER, $this->_config)) { + $profiler = $this->_config[Db::PROFILER]; + unset($this->_config[Db::PROFILER]); + } + $this->setProfiler($profiler); + } + + /** + * Check for config options that are mandatory. + * Throw exceptions if any are missing. + * + * @param array $config + * @throws AdapterException + */ + protected function _checkRequiredOptions(array $config) + { + // we need at least a dbname + if (! array_key_exists('dbname', $config)) { + /** @see AdapterException */ + throw new AdapterException( + "Configuration array must have a key for 'dbname' that names the database instance" + ); + } + + if (! array_key_exists('password', $config)) { + /** + * @see AdapterException + */ + throw new AdapterException("Configuration array must have a key for 'password' for login credentials"); + } + + if (! array_key_exists('username', $config)) { + /** + * @see AdapterException + */ + throw new AdapterException("Configuration array must have a key for 'username' for login credentials"); + } + } + + /** + * Returns the underlying database connection object or resource. + * If not presently connected, this initiates the connection. + * + * @return object|resource|null + */ + public function getConnection() + { + $this->_connect(); + return $this->_connection; + } + + /** + * Returns the configuration variables in this adapter. + * + * @return array + */ + public function getConfig() + { + return $this->_config; + } + + /** + * Set the adapter's profiler object. + * + * The argument may be a boolean, an associative array, an instance of + * Zend_Db_Profiler, or an instance of Zend_Config. + * + * A boolean argument sets the profiler to enabled if true, or disabled if + * false. The profiler class is the adapter's default profiler class, + * Zend_Db_Profiler. + * + * An instance of Zend_Db_Profiler sets the adapter's instance to that + * object. The profiler is enabled and disabled separately. + * + * An associative array argument may contain any of the keys 'enabled', + * 'class', and 'instance'. The 'enabled' and 'instance' keys correspond to the + * boolean and object types documented above. The 'class' key is used to name a + * class to use for a custom profiler. The class must be Zend_Db_Profiler or a + * subclass. The class is instantiated with no constructor arguments. The 'class' + * option is ignored when the 'instance' option is supplied. + * + * An object of type Zend_Config may contain the properties 'enabled', 'class', and + * 'instance', just as if an associative array had been passed instead. + * + * @param Profiler|Zend_Config|array|boolean $profiler + * @return Adapter Provides a fluent interface + * @throws ProfilerException if the object instance or class specified + * is not Zend_Db_Profiler or an extension of that class. + */ + public function setProfiler($profiler) + { + $enabled = null; + $profilerClass = $this->_defaultProfilerClass; + $profilerInstance = null; + + if ($profilerIsObject = is_object($profiler)) { + if ($profiler instanceof Profiler) { + $profilerInstance = $profiler; + } elseif ($profiler instanceof Zend_Config) { + $profiler = $profiler->toArray(); + } else { + throw new ProfilerException('Profiler argument must be an instance of either Profiler' + . ' or Zend_Config when provided as an object'); + } + } + + if (is_array($profiler)) { + if (isset($profiler['enabled'])) { + $enabled = (bool) $profiler['enabled']; + } + if (isset($profiler['class'])) { + $profilerClass = $profiler['class']; + } + if (isset($profiler['instance'])) { + $profilerInstance = $profiler['instance']; + } + } elseif (!$profilerIsObject) { + $enabled = (bool) $profiler; + } + + if ($profilerInstance === null) { + $profilerInstance = new $profilerClass(); + } + + if (!$profilerInstance instanceof Profiler) { + throw new ProfilerException('Class ' . get_class($profilerInstance) . ' does not extend ' + . 'Zend_Db_Profiler'); + } + + if (null !== $enabled) { + $profilerInstance->setEnabled($enabled); + } + + $this->_profiler = $profilerInstance; + + return $this; + } + + + /** + * Returns the profiler for this adapter. + * + * @return Profiler + */ + public function getProfiler() + { + return $this->_profiler; + } + + /** + * Get the default statement class. + * + * @return string + */ + public function getStatementClass() + { + return $this->_defaultStmtClass; + } + + /** + * Set the default statement class. + * + * @return Adapter Fluent interface + */ + public function setStatementClass($class) + { + $this->_defaultStmtClass = $class; + return $this; + } + + /** + * Prepares and executes an SQL statement with bound data. + * + * @param mixed $sql The SQL statement with placeholders. + * May be a string or Zend_Db_Select. + * @param mixed $bind An array of data to bind to the placeholders. + * @return StatementInterface + */ + public function query($sql, $bind = array()) + { + // connect to the database if needed + $this->_connect(); + + // is the $sql a Zend_Db_Select object? + if ($sql instanceof Select) { + if (empty($bind)) { + $bind = $sql->getBind(); + } + + $sql = $sql->assemble(); + } + + // make sure $bind to an array; + // don't use (array) typecasting because + // because $bind may be a Zend_Db_Expr object + if (!is_array($bind)) { + $bind = array($bind); + } + + // prepare and execute the statement with profiling + $stmt = $this->prepare($sql); + $stmt->execute($bind); + + // return the results embedded in the prepared statement object + $stmt->setFetchMode($this->_fetchMode); + return $stmt; + } + + /** + * Leave autocommit mode and begin a transaction. + * + * @return Adapter + */ + public function beginTransaction() + { + $this->_connect(); + $q = $this->_profiler->queryStart('begin', Profiler::TRANSACTION); + $this->_beginTransaction(); + $this->_profiler->queryEnd($q); + return $this; + } + + /** + * Commit a transaction and return to autocommit mode. + * + * @return Adapter + */ + public function commit() + { + $this->_connect(); + $q = $this->_profiler->queryStart('commit', Profiler::TRANSACTION); + $this->_commit(); + $this->_profiler->queryEnd($q); + return $this; + } + + /** + * Roll back a transaction and return to autocommit mode. + * + * @return Adapter + */ + public function rollBack() + { + $this->_connect(); + $q = $this->_profiler->queryStart('rollback', Profiler::TRANSACTION); + $this->_rollBack(); + $this->_profiler->queryEnd($q); + return $this; + } + + /** + * Inserts a table row with specified data. + * + * @param mixed $table The table to insert data into. + * @param array $bind Column-value pairs. + * @return int The number of affected rows. + * @throws AdapterException|\gipfl\ZfDb\Statement\Exception\StatementException + */ + public function insert($table, array $bind) + { + // extract and quote col names from the array keys + $cols = array(); + $vals = array(); + $i = 0; + foreach ($bind as $col => $val) { + $cols[] = $this->quoteIdentifier($col, true); + if ($val instanceof Expr) { + $vals[] = $val->__toString(); + unset($bind[$col]); + } else { + if ($this->supportsParameters('positional')) { + $vals[] = '?'; + } else { + if ($this->supportsParameters('named')) { + unset($bind[$col]); + $bind[':col'.$i] = $val; + $vals[] = ':col'.$i; + $i++; + } else { + throw new AdapterException(get_class($this) ." doesn't support positional or named binding"); + } + } + } + } + + // build the statement + $sql = "INSERT INTO " + . $this->quoteIdentifier($table, true) + . ' (' . implode(', ', $cols) . ') ' + . 'VALUES (' . implode(', ', $vals) . ')'; + + // execute the statement and return the number of affected rows + if ($this->supportsParameters('positional')) { + $bind = array_values($bind); + } + $stmt = $this->query($sql, $bind); + $result = $stmt->rowCount(); + return $result; + } + + /** + * Updates table rows with specified data based on a WHERE clause. + * + * @param mixed $table The table to update. + * @param array $bind Column-value pairs. + * @param mixed $where UPDATE WHERE clause(s). + * @return int The number of affected rows. + * @throws AdapterException + */ + public function update($table, array $bind, $where = '') + { + /** + * Build "col = ?" pairs for the statement, + * except for Zend_Db_Expr which is treated literally. + */ + $set = array(); + $i = 0; + foreach ($bind as $col => $val) { + if ($val instanceof Expr) { + $val = $val->__toString(); + unset($bind[$col]); + } else { + if ($this->supportsParameters('positional')) { + $val = '?'; + } else { + if ($this->supportsParameters('named')) { + unset($bind[$col]); + $bind[':col'.$i] = $val; + $val = ':col'.$i; + $i++; + } else { + /** @see AdapterException */ + throw new AdapterException(get_class($this) ." doesn't support positional or named binding"); + } + } + } + $set[] = $this->quoteIdentifier($col, true) . ' = ' . $val; + } + + $where = $this->_whereExpr($where); + + /** + * Build the UPDATE statement + */ + $sql = "UPDATE " + . $this->quoteIdentifier($table, true) + . ' SET ' . implode(', ', $set) + . (($where) ? " WHERE $where" : ''); + + /** + * Execute the statement and return the number of affected rows + */ + if ($this->supportsParameters('positional')) { + $stmt = $this->query($sql, array_values($bind)); + } else { + $stmt = $this->query($sql, $bind); + } + $result = $stmt->rowCount(); + return $result; + } + + /** + * Deletes table rows based on a WHERE clause. + * + * @param mixed $table The table to update. + * @param mixed $where DELETE WHERE clause(s). + * @return int The number of affected rows. + */ + public function delete($table, $where = '') + { + $where = $this->_whereExpr($where); + + /** + * Build the DELETE statement + */ + $sql = "DELETE FROM " + . $this->quoteIdentifier($table, true) + . (($where) ? " WHERE $where" : ''); + + /** + * Execute the statement and return the number of affected rows + */ + $stmt = $this->query($sql); + $result = $stmt->rowCount(); + return $result; + } + + /** + * Convert an array, string, or Zend_Db_Expr object + * into a string to put in a WHERE clause. + * + * @param mixed $where + * @return string + */ + protected function _whereExpr($where) + { + if (empty($where)) { + return $where; + } + if (!is_array($where)) { + $where = array($where); + } + foreach ($where as $cond => &$term) { + // is $cond an int? (i.e. Not a condition) + if (is_int($cond)) { + // $term is the full condition + if ($term instanceof Expr) { + $term = $term->__toString(); + } + } else { + // $cond is the condition with placeholder, + // and $term is quoted into the condition + $term = $this->quoteInto($cond, $term); + } + $term = '(' . $term . ')'; + } + + $where = implode(' AND ', $where); + return $where; + } + + /** + * Creates and returns a new Zend_Db_Select object for this adapter. + * + * @return Select + */ + public function select() + { + return new Select($this); + } + + /** + * Get the fetch mode. + * + * @return int + */ + public function getFetchMode() + { + return $this->_fetchMode; + } + + /** + * Fetches all SQL result rows as a sequential array. + * Uses the current fetchMode for the adapter. + * + * @param string|Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @param mixed $fetchMode Override current fetch mode. + * @return array + */ + public function fetchAll($sql, $bind = array(), $fetchMode = null) + { + if ($fetchMode === null) { + $fetchMode = $this->_fetchMode; + } + $stmt = $this->query($sql, $bind); + $result = $stmt->fetchAll($fetchMode); + return $result; + } + + /** + * Fetches the first row of the SQL result. + * Uses the current fetchMode for the adapter. + * + * @param string|Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @param mixed $fetchMode Override current fetch mode. + * @return mixed Array, object, or scalar depending on fetch mode. + */ + public function fetchRow($sql, $bind = array(), $fetchMode = null) + { + if ($fetchMode === null) { + $fetchMode = $this->_fetchMode; + } + $stmt = $this->query($sql, $bind); + $result = $stmt->fetch($fetchMode); + return $result; + } + + /** + * Fetches all SQL result rows as an associative array. + * + * The first column is the key, the entire row array is the + * value. You should construct the query to be sure that + * the first column contains unique values, or else + * rows with duplicate values in the first column will + * overwrite previous data. + * + * @param string|Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @return array + */ + public function fetchAssoc($sql, $bind = array()) + { + $stmt = $this->query($sql, $bind); + $data = array(); + while ($row = $stmt->fetch(Db::FETCH_ASSOC)) { + $tmp = array_values(array_slice($row, 0, 1)); + $data[$tmp[0]] = $row; + } + return $data; + } + + /** + * Fetches the first column of all SQL result rows as an array. + * + * @param string|Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @return array + */ + public function fetchCol($sql, $bind = array()) + { + $stmt = $this->query($sql, $bind); + $result = $stmt->fetchAll(Db::FETCH_COLUMN, 0); + return $result; + } + + /** + * Fetches all SQL result rows as an array of key-value pairs. + * + * The first column is the key, the second column is the + * value. + * + * @param string|Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @return array + */ + public function fetchPairs($sql, $bind = array()) + { + $stmt = $this->query($sql, $bind); + $data = array(); + while ($row = $stmt->fetch(Db::FETCH_NUM)) { + $data[$row[0]] = $row[1]; + } + return $data; + } + + /** + * Fetches the first column of the first row of the SQL result. + * + * @param string|Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @return string + */ + public function fetchOne($sql, $bind = array()) + { + $stmt = $this->query($sql, $bind); + $result = $stmt->fetchColumn(0); + return $result; + } + + /** + * Quote a raw string. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (is_int($value)) { + return $value; + } elseif (is_float($value)) { + return sprintf('%F', $value); + } + return "'" . addcslashes($value, "\000\n\r\\'\"\032") . "'"; + } + + /** + * Safely quotes a value for an SQL statement. + * + * If an array is passed as the value, the array values are quoted + * and then returned as a comma-separated string. + * + * @param mixed $value The value to quote. + * @param mixed $type OPTIONAL the SQL datatype name, or constant, or null. + * @return mixed An SQL-safe quoted value (or string of separated values). + */ + public function quote($value, $type = null) + { + $this->_connect(); + + if ($value instanceof Select) { + return '(' . $value->assemble() . ')'; + } + + if ($value instanceof Expr) { + return $value->__toString(); + } + + if (is_array($value)) { + foreach ($value as &$val) { + $val = $this->quote($val, $type); + } + return implode(', ', $value); + } + + if ($type !== null && array_key_exists($type = strtoupper($type), $this->_numericDataTypes)) { + $quotedValue = '0'; + switch ($this->_numericDataTypes[$type]) { + case Db::INT_TYPE: // 32-bit integer + $quotedValue = (string) intval($value); + break; + case Db::BIGINT_TYPE: // 64-bit integer + // ANSI SQL-style hex literals (e.g. x'[\dA-F]+') + // are not supported here, because these are string + // literals, not numeric literals. + if (preg_match( + '/^( + [+-]? # optional sign + (?: + 0[Xx][\da-fA-F]+ # ODBC-style hexadecimal + |\d+ # decimal or octal, or MySQL ZEROFILL decimal + (?:[eE][+-]?\d+)? # optional exponent on decimals or octals + ) + )/x', + (string) $value, + $matches + )) { + $quotedValue = $matches[1]; + } + break; + case Db::FLOAT_TYPE: // float or decimal + $quotedValue = sprintf('%F', $value); + } + return $quotedValue; + } + + return $this->_quote($value); + } + + /** + * Quotes a value and places into a piece of text at a placeholder. + * + * The placeholder is a question-mark; all placeholders will be replaced + * with the quoted value. For example: + * + * <code> + * $text = "WHERE date < ?"; + * $date = "2005-01-02"; + * $safe = $sql->quoteInto($text, $date); + * // $safe = "WHERE date < '2005-01-02'" + * </code> + * + * @param string $text The text with a placeholder. + * @param mixed $value The value to quote. + * @param string $type OPTIONAL SQL datatype + * @param integer $count OPTIONAL count of placeholders to replace + * @return string An SQL-safe quoted value placed into the original text. + */ + public function quoteInto($text, $value, $type = null, $count = null) + { + if ($count === null) { + return str_replace('?', $this->quote($value, $type), $text); + } else { + return implode($this->quote($value, $type), explode('?', $text, $count + 1)); + } + } + + /** + * Quotes an identifier. + * + * Accepts a string representing a qualified indentifier. For Example: + * <code> + * $adapter->quoteIdentifier('myschema.mytable') + * </code> + * Returns: "myschema"."mytable" + * + * Or, an array of one or more identifiers that may form a qualified identifier: + * <code> + * $adapter->quoteIdentifier(array('myschema','my.table')) + * </code> + * Returns: "myschema"."my.table" + * + * The actual quote character surrounding the identifiers may vary depending on + * the adapter. + * + * @param string|array|Expr $ident The identifier. + * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option. + * @return string The quoted identifier. + */ + public function quoteIdentifier($ident, $auto = false) + { + return $this->_quoteIdentifierAs($ident, null, $auto); + } + + /** + * Quote a column identifier and alias. + * + * @param string|array|Expr $ident The identifier or expression. + * @param string $alias An alias for the column. + * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option. + * @return string The quoted identifier and alias. + */ + public function quoteColumnAs($ident, $alias, $auto = false) + { + return $this->_quoteIdentifierAs($ident, $alias, $auto); + } + + /** + * Quote a table identifier and alias. + * + * @param string|array|Expr $ident The identifier or expression. + * @param string $alias An alias for the table. + * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option. + * @return string The quoted identifier and alias. + */ + public function quoteTableAs($ident, $alias = null, $auto = false) + { + return $this->_quoteIdentifierAs($ident, $alias, $auto); + } + + /** + * Quote an identifier and an optional alias. + * + * @param string|array|Expr $ident The identifier or expression. + * @param string $alias An optional alias. + * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option. + * @param string $as The string to add between the identifier/expression and the alias. + * @return string The quoted identifier and alias. + */ + protected function _quoteIdentifierAs($ident, $alias = null, $auto = false, $as = ' AS ') + { + if ($ident instanceof Expr) { + $quoted = $ident->__toString(); + } elseif ($ident instanceof Select) { + $quoted = '(' . $ident->assemble() . ')'; + } else { + if (is_string($ident)) { + $ident = explode('.', $ident); + } + if (is_array($ident)) { + $segments = array(); + foreach ($ident as $segment) { + if ($segment instanceof Expr) { + $segments[] = $segment->__toString(); + } else { + $segments[] = $this->_quoteIdentifier($segment, $auto); + } + } + if ($alias !== null && end($ident) == $alias) { + $alias = null; + } + $quoted = implode('.', $segments); + } else { + $quoted = $this->_quoteIdentifier($ident, $auto); + } + } + if ($alias !== null) { + $quoted .= $as . $this->_quoteIdentifier($alias, $auto); + } + return $quoted; + } + + /** + * Quote an identifier. + * + * @param string $value The identifier or expression. + * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option. + * @return string The quoted identifier and alias. + */ + protected function _quoteIdentifier($value, $auto = false) + { + if ($auto === false || $this->_autoQuoteIdentifiers === true) { + $q = $this->getQuoteIdentifierSymbol(); + return ($q . str_replace("$q", "$q$q", $value) . $q); + } + return $value; + } + + /** + * Returns the symbol the adapter uses for delimited identifiers. + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return '"'; + } + + /** + * Return the most recent value from the specified sequence in the database. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function lastSequenceId($sequenceName) + { + return null; + } + + /** + * Generate a new value from the specified sequence in the database, and return it. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function nextSequenceId($sequenceName) + { + return null; + } + + /** + * Helper method to change the case of the strings used + * when returning result sets in FETCH_ASSOC and FETCH_BOTH + * modes. + * + * This is not intended to be used by application code, + * but the method must be public so the Statement class + * can invoke it. + * + * @param string $key + * @return string + */ + public function foldCase($key) + { + switch ($this->_caseFolding) { + case Db::CASE_LOWER: + $value = strtolower((string) $key); + break; + case Db::CASE_UPPER: + $value = strtoupper((string) $key); + break; + case Db::CASE_NATURAL: + default: + $value = (string) $key; + } + return $value; + } + + /** + * called when object is getting serialized + * This disconnects the DB object that cant be serialized + * + * @return array + * @throws AdapterException + */ + public function __sleep() + { + if ($this->_allowSerialization == false) { + /** @see AdapterException */ + throw new AdapterException( + get_class($this) . ' is not allowed to be serialized' + ); + } + $this->_connection = null; + + return array_keys( + array_diff_key(get_object_vars($this), array('_connection' => null)) + ); + } + + /** + * called when object is getting unserialized + * + * @return void + */ + public function __wakeup() + { + if ($this->_autoReconnectOnUnserialize == true) { + $this->getConnection(); + } + } + + /** + * Abstract Methods + */ + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + abstract public function listTables(); + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + abstract public function describeTable($tableName, $schemaName = null); + + /** + * Creates a connection to the database. + * + * @return void + */ + abstract protected function _connect(); + + /** + * Test if a connection is active + * + * @return boolean + */ + abstract public function isConnected(); + + /** + * Force the connection to close. + * + * @return void + */ + abstract public function closeConnection(); + + /** + * Prepare a statement and return a PDOStatement-like object. + * + * @param string|Select $sql SQL query + * @return Statement|\PDOStatement + */ + abstract public function prepare($sql); + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + */ + abstract public function lastInsertId($tableName = null, $primaryKey = null); + + /** + * Begin a transaction. + */ + abstract protected function _beginTransaction(); + + /** + * Commit a transaction. + */ + abstract protected function _commit(); + + /** + * Roll-back a transaction. + */ + abstract protected function _rollBack(); + + /** + * Set the fetch mode. + * + * @param integer $mode + * @return void + * @throws AdapterException + */ + abstract public function setFetchMode($mode); + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param mixed $sql + * @param integer $count + * @param integer $offset + * @return string + */ + abstract public function limit($sql, $count, $offset = 0); + + /** + * Check if the adapter supports real SQL parameters. + * + * @param string $type 'positional' or 'named' + * @return bool + */ + abstract public function supportsParameters($type); + + /** + * Retrieve server version in PHP style + * + * @return string + */ + abstract public function getServerVersion(); +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Db2.php b/vendor/gipfl/zfdb/src/Adapter/Db2.php new file mode 100644 index 0000000..55dce72 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Db2.php @@ -0,0 +1,792 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + * + */ +namespace gipfl\ZfDb\Adapter; + +use gipfl\ZfDb\Adapter\Exception\AdapterExceptionDb2; +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Statement\Db2Statement; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +class Db2 extends Adapter +{ + /** + * User-provided configuration. + * + * Basic keys are: + * + * username => (string) Connect to the database as this username. + * password => (string) Password associated with the username. + * host => (string) What host to connect to (default 127.0.0.1) + * dbname => (string) The name of the database to user + * protocol => (string) Protocol to use, defaults to "TCPIP" + * port => (integer) Port number to use for TCP/IP if protocol is "TCPIP" + * persistent => (boolean) Set TRUE to use a persistent connection (db2_pconnect) + * os => (string) This should be set to 'i5' if the db is on an os400/i5 + * schema => (string) The default schema the connection should use + * + * @var array + */ + protected $_config = array( + 'dbname' => null, + 'username' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => '50000', + 'protocol' => 'TCPIP', + 'persistent' => false, + 'os' => null, + 'schema' => null + ); + + /** + * Execution mode + * + * @var int execution flag (DB2_AUTOCOMMIT_ON or DB2_AUTOCOMMIT_OFF) + */ + protected $_execute_mode = DB2_AUTOCOMMIT_ON; + + /** + * Default class name for a DB statement. + * + * @var string + */ + protected $_defaultStmtClass = Db2Statement::class; + protected $_isI5 = false; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INTEGER' => Db::INT_TYPE, + 'SMALLINT' => Db::INT_TYPE, + 'BIGINT' => Db::BIGINT_TYPE, + 'DECIMAL' => Db::FLOAT_TYPE, + 'NUMERIC' => Db::FLOAT_TYPE + ); + + /** + * Creates a connection resource. + * + * @return void + */ + protected function _connect() + { + if (is_resource($this->_connection)) { + // connection already exists + return; + } + + if (!extension_loaded('ibm_db2')) { + throw new AdapterExceptionDb2( + 'The IBM DB2 extension is required for this adapter but the extension is not loaded' + ); + } + + $this->_determineI5(); + if ($this->_config['persistent']) { + // use persistent connection + $conn_func_name = 'db2_pconnect'; + } else { + // use "normal" connection + $conn_func_name = 'db2_connect'; + } + + if (!isset($this->_config['driver_options']['autocommit'])) { + // set execution mode + $this->_config['driver_options']['autocommit'] = &$this->_execute_mode; + } + + if (isset($this->_config['options'][Db::CASE_FOLDING])) { + $caseAttrMap = array( + Db::CASE_NATURAL => DB2_CASE_NATURAL, + Db::CASE_UPPER => DB2_CASE_UPPER, + Db::CASE_LOWER => DB2_CASE_LOWER + ); + $this->_config['driver_options']['DB2_ATTR_CASE'] + = $caseAttrMap[$this->_config['options'][Db::CASE_FOLDING]]; + } + + if ($this->_isI5 && isset($this->_config['driver_options']['i5_naming'])) { + if ($this->_config['driver_options']['i5_naming']) { + $this->_config['driver_options']['i5_naming'] = DB2_I5_NAMING_ON; + } else { + $this->_config['driver_options']['i5_naming'] = DB2_I5_NAMING_OFF; + } + } + + if ($this->_config['host'] !== 'localhost' && !$this->_isI5) { + // if the host isn't localhost, use extended connection params + $dbname = 'DRIVER={IBM DB2 ODBC DRIVER}' . + ';DATABASE=' . $this->_config['dbname'] . + ';HOSTNAME=' . $this->_config['host'] . + ';PORT=' . $this->_config['port'] . + ';PROTOCOL=' . $this->_config['protocol'] . + ';UID=' . $this->_config['username'] . + ';PWD=' . $this->_config['password'] .';'; + $this->_connection = $conn_func_name( + $dbname, + null, + null, + $this->_config['driver_options'] + ); + } else { + // host is localhost, so use standard connection params + $this->_connection = $conn_func_name( + $this->_config['dbname'], + $this->_config['username'], + $this->_config['password'], + $this->_config['driver_options'] + ); + } + + // check the connection + if (!$this->_connection) { + throw new AdapterExceptionDb2(db2_conn_errormsg(), db2_conn_error()); + } + } + + /** + * Test if a connection is active + * + * @return boolean + */ + public function isConnected() + { + return ((bool) (is_resource($this->_connection) + && get_resource_type($this->_connection) == 'DB2 Connection')); + } + + /** + * Force the connection to close. + * + * @return void + */ + public function closeConnection() + { + if ($this->isConnected()) { + db2_close($this->_connection); + } + $this->_connection = null; + } + + /** + * Returns an SQL statement for preparation. + * + * @param string $sql The SQL statement with placeholders. + * @return Db2Statement + */ + public function prepare($sql) + { + $this->_connect(); + $stmtClass = $this->_defaultStmtClass; + $stmt = new $stmtClass($this, $sql); + $stmt->setFetchMode($this->_fetchMode); + return $stmt; + } + + /** + * Gets the execution mode + * + * @return int the execution mode (DB2_AUTOCOMMIT_ON or DB2_AUTOCOMMIT_OFF) + */ + public function _getExecuteMode() + { + return $this->_execute_mode; + } + + /** + * @param integer $mode + * @return void + */ + public function _setExecuteMode($mode) + { + switch ($mode) { + case DB2_AUTOCOMMIT_OFF: + case DB2_AUTOCOMMIT_ON: + $this->_execute_mode = $mode; + db2_autocommit($this->_connection, $mode); + break; + default: + /** + * @see AdapterExceptionDb2 + */ + throw new AdapterExceptionDb2("execution mode not supported"); + break; + } + } + + /** + * Quote a raw string. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (is_int($value) || is_float($value)) { + return $value; + } + /** + * Use db2_escape_string() if it is present in the IBM DB2 extension. + * But some supported versions of PHP do not include this function, + * so fall back to default quoting in the parent class. + */ + if (function_exists('db2_escape_string')) { + return "'" . db2_escape_string($value) . "'"; + } + return parent::_quote($value); + } + + /** + * @return string + */ + public function getQuoteIdentifierSymbol() + { + $this->_connect(); + $info = db2_server_info($this->_connection); + if ($info) { + $identQuote = $info->IDENTIFIER_QUOTE_CHAR; + } else { + // db2_server_info() does not return result on some i5 OS version + if ($this->_isI5) { + $identQuote ="'"; + } + } + return $identQuote; + } + + /** + * Returns a list of the tables in the database. + * @param string $schema OPTIONAL + * @return array + */ + public function listTables($schema = null) + { + $this->_connect(); + + if ($schema === null && $this->_config['schema'] != null) { + $schema = $this->_config['schema']; + } + + $tables = array(); + + if (!$this->_isI5) { + if ($schema) { + $stmt = db2_tables($this->_connection, null, $schema); + } else { + $stmt = db2_tables($this->_connection); + } + while ($row = db2_fetch_assoc($stmt)) { + $tables[] = $row['TABLE_NAME']; + } + } else { + $tables = $this->_i5listTables($schema); + } + + return $tables; + } + + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * DB2 not supports UNSIGNED integer. + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + // Ensure the connection is made so that _isI5 is set + $this->_connect(); + + if ($schemaName === null && $this->_config['schema'] != null) { + $schemaName = $this->_config['schema']; + } + + if (!$this->_isI5) { + $sql = "SELECT DISTINCT c.tabschema, c.tabname, c.colname, c.colno, + c.typename, c.default, c.nulls, c.length, c.scale, + c.identity, tc.type AS tabconsttype, k.colseq + FROM syscat.columns c + LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc + ON (k.tabschema = tc.tabschema + AND k.tabname = tc.tabname + AND tc.type = 'P')) + ON (c.tabschema = k.tabschema + AND c.tabname = k.tabname + AND c.colname = k.colname) + WHERE " + . $this->quoteInto('UPPER(c.tabname) = UPPER(?)', $tableName); + + if ($schemaName) { + $sql .= $this->quoteInto(' AND UPPER(c.tabschema) = UPPER(?)', $schemaName); + } + + $sql .= " ORDER BY c.colno"; + } else { + // DB2 On I5 specific query + $sql = "SELECT DISTINCT C.TABLE_SCHEMA, C.TABLE_NAME, C.COLUMN_NAME, C.ORDINAL_POSITION, + C.DATA_TYPE, C.COLUMN_DEFAULT, C.NULLS ,C.LENGTH, C.SCALE, LEFT(C.IDENTITY,1), + LEFT(tc.TYPE, 1) AS tabconsttype, k.COLSEQ + FROM QSYS2.SYSCOLUMNS C + LEFT JOIN (QSYS2.syskeycst k JOIN QSYS2.SYSCST tc + ON (k.TABLE_SCHEMA = tc.TABLE_SCHEMA + AND k.TABLE_NAME = tc.TABLE_NAME + AND LEFT(tc.type,1) = 'P')) + ON (C.TABLE_SCHEMA = k.TABLE_SCHEMA + AND C.TABLE_NAME = k.TABLE_NAME + AND C.COLUMN_NAME = k.COLUMN_NAME) + WHERE " + . $this->quoteInto('UPPER(C.TABLE_NAME) = UPPER(?)', $tableName); + + if ($schemaName) { + $sql .= $this->quoteInto(' AND UPPER(C.TABLE_SCHEMA) = UPPER(?)', $schemaName); + } + + $sql .= " ORDER BY C.ORDINAL_POSITION FOR FETCH ONLY"; + } + + $desc = array(); + $stmt = $this->query($sql); + + /** + * To avoid case issues, fetch using FETCH_NUM + */ + $result = $stmt->fetchAll(Db::FETCH_NUM); + + /** + * The ordering of columns is defined by the query so we can map + * to variables to improve readability + */ + $tabschema = 0; + $tabname = 1; + $colname = 2; + $colno = 3; + $typename = 4; + $default = 5; + $nulls = 6; + $length = 7; + $scale = 8; + $identityCol = 9; + $tabconstType = 10; + $colseq = 11; + + foreach ($result as $key => $row) { + list ($primary, $primaryPosition, $identity) = array(false, null, false); + if ($row[$tabconstType] == 'P') { + $primary = true; + $primaryPosition = $row[$colseq]; + } + /** + * In IBM DB2, an column can be IDENTITY + * even if it is not part of the PRIMARY KEY. + */ + if ($row[$identityCol] == 'Y') { + $identity = true; + } + + // only colname needs to be case adjusted + $desc[$this->foldCase($row[$colname])] = array( + 'SCHEMA_NAME' => $this->foldCase($row[$tabschema]), + 'TABLE_NAME' => $this->foldCase($row[$tabname]), + 'COLUMN_NAME' => $this->foldCase($row[$colname]), + 'COLUMN_POSITION' => (!$this->_isI5) ? $row[$colno]+1 : $row[$colno], + 'DATA_TYPE' => $row[$typename], + 'DEFAULT' => $row[$default], + 'NULLABLE' => (bool) ($row[$nulls] == 'Y'), + 'LENGTH' => $row[$length], + 'SCALE' => $row[$scale], + 'PRECISION' => ($row[$typename] == 'DECIMAL' ? $row[$length] : 0), + 'UNSIGNED' => false, + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + + return $desc; + } + + /** + * Return the most recent value from the specified sequence in the database. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function lastSequenceId($sequenceName) + { + $this->_connect(); + + if (!$this->_isI5) { + $quotedSequenceName = $this->quoteIdentifier($sequenceName, true); + $sql = 'SELECT PREVVAL FOR ' . $quotedSequenceName . ' AS VAL FROM SYSIBM.SYSDUMMY1'; + } else { + $quotedSequenceName = $sequenceName; + $sql = 'SELECT PREVVAL FOR ' . $this->quoteIdentifier($sequenceName, true) . ' AS VAL FROM QSYS2.QSQPTABL'; + } + + $value = $this->fetchOne($sql); + return (string) $value; + } + + /** + * Generate a new value from the specified sequence in the database, and return it. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function nextSequenceId($sequenceName) + { + $this->_connect(); + $sql = 'SELECT NEXTVAL FOR '.$this->quoteIdentifier($sequenceName, true).' AS VAL FROM SYSIBM.SYSDUMMY1'; + $value = $this->fetchOne($sql); + return (string) $value; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * The IDENTITY_VAL_LOCAL() function gives the last generated identity value + * in the current process, even if it was for a GENERATED column. + * + * @param string $tableName OPTIONAL + * @param string $primaryKey OPTIONAL + * @param string $idType OPTIONAL used for i5 platform to define sequence/idenity unique value + * @return string + */ + + public function lastInsertId($tableName = null, $primaryKey = null, $idType = null) + { + $this->_connect(); + + if ($this->_isI5) { + return (string) $this->_i5LastInsertId($tableName, $idType); + } + + if ($tableName !== null) { + $sequenceName = $tableName; + if ($primaryKey) { + $sequenceName .= "_$primaryKey"; + } + $sequenceName .= '_seq'; + return $this->lastSequenceId($sequenceName); + } + + $sql = 'SELECT IDENTITY_VAL_LOCAL() AS VAL FROM SYSIBM.SYSDUMMY1'; + $value = $this->fetchOne($sql); + return (string) $value; + } + + /** + * Begin a transaction. + * + * @return void + */ + protected function _beginTransaction() + { + $this->_setExecuteMode(DB2_AUTOCOMMIT_OFF); + } + + /** + * Commit a transaction. + * + * @return void + */ + protected function _commit() + { + if (!db2_commit($this->_connection)) { + /** + * @see AdapterExceptionDb2 + */ + throw new AdapterExceptionDb2( + db2_conn_errormsg($this->_connection), + db2_conn_error($this->_connection) + ); + } + + $this->_setExecuteMode(DB2_AUTOCOMMIT_ON); + } + + /** + * Rollback a transaction. + * + * @return void + */ + protected function _rollBack() + { + if (!db2_rollback($this->_connection)) { + /** + * @see AdapterExceptionDb2 + */ + throw new AdapterExceptionDb2( + db2_conn_errormsg($this->_connection), + db2_conn_error($this->_connection) + ); + } + $this->_setExecuteMode(DB2_AUTOCOMMIT_ON); + } + + /** + * Set the fetch mode. + * + * @param integer $mode + * @return void + * @throws AdapterExceptionDb2 + */ + public function setFetchMode($mode) + { + switch ($mode) { + case Db::FETCH_NUM: // seq array + case Db::FETCH_ASSOC: // assoc array + case Db::FETCH_BOTH: // seq+assoc array + case Db::FETCH_OBJ: // object + $this->_fetchMode = $mode; + break; + case Db::FETCH_BOUND: // bound to PHP variable + throw new AdapterExceptionDb2('FETCH_BOUND is not supported yet'); + default: + throw new AdapterExceptionDb2("Invalid fetch mode '$mode' specified"); + } + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterExceptionDb2("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterExceptionDb2("LIMIT argument offset=$offset is not valid"); + } + + if ($offset == 0) { + $limit_sql = $sql . " FETCH FIRST $count ROWS ONLY"; + return $limit_sql; + } + + /** + * DB2 does not implement the LIMIT clause as some RDBMS do. + * We have to simulate it with subqueries and ROWNUM. + * Unfortunately because we use the column wildcard "*", + * this puts an extra column into the query result set. + */ + $limit_sql = "SELECT z2.* + FROM ( + SELECT ROW_NUMBER() OVER() AS \"ZEND_DB_ROWNUM\", z1.* + FROM ( + " . $sql . " + ) z1 + ) z2 + WHERE z2.zend_db_rownum BETWEEN " . ($offset+1) . " AND " . ($offset+$count); + return $limit_sql; + } + + /** + * Check if the adapter supports real SQL parameters. + * + * @param string $type 'positional' or 'named' + * @return bool + */ + public function supportsParameters($type) + { + if ($type == 'positional') { + return true; + } + + // if its 'named' or anything else + return false; + } + + /** + * Retrieve server version in PHP style + * + * @return string + */ + public function getServerVersion() + { + $this->_connect(); + $server_info = db2_server_info($this->_connection); + if ($server_info !== false) { + $version = $server_info->DBMS_VER; + if ($this->_isI5) { + $version = (int) substr($version, 0, 2) + . '.' . (int) substr($version, 2, 2) + . '.' . (int) substr($version, 4); + } + return $version; + } else { + return null; + } + } + + /** + * Return whether or not this is running on i5 + * + * @return bool + */ + public function isI5() + { + if ($this->_isI5 === null) { + $this->_determineI5(); + } + + return (bool) $this->_isI5; + } + + /** + * Check the connection parameters according to verify + * type of used OS + * + * @return void + */ + protected function _determineI5() + { + // first us the compiled flag. + $this->_isI5 = (php_uname('s') == 'OS400') ? true : false; + + // if this is set, then us it + if (isset($this->_config['os'])) { + if (strtolower($this->_config['os']) === 'i5') { + $this->_isI5 = true; + } else { + // any other value passed in, its null + $this->_isI5 = false; + } + } + } + + /** + * Db2 On I5 specific method + * + * Returns a list of the tables in the database . + * Used only for DB2/400. + * + * @return array + */ + protected function _i5listTables($schema = null) + { + //list of i5 libraries. + $tables = array(); + if ($schema) { + $tablesStatement = db2_tables($this->_connection, null, $schema); + while ($rowTables = db2_fetch_assoc($tablesStatement)) { + if ($rowTables['TABLE_NAME'] !== null) { + $tables[] = $rowTables['TABLE_NAME']; + } + } + } else { + $schemaStatement = db2_tables($this->_connection); + while ($schema = db2_fetch_assoc($schemaStatement)) { + if ($schema['TABLE_SCHEM'] !== null) { + // list of the tables which belongs to the selected library + $tablesStatement = db2_tables($this->_connection, null, $schema['TABLE_SCHEM']); + if (is_resource($tablesStatement)) { + while ($rowTables = db2_fetch_assoc($tablesStatement)) { + if ($rowTables['TABLE_NAME'] !== null) { + $tables[] = $rowTables['TABLE_NAME']; + } + } + } + } + } + } + + return $tables; + } + + protected function _i5LastInsertId($objectName = null, $idType = null) + { + + if ($objectName === null) { + $sql = 'SELECT IDENTITY_VAL_LOCAL() AS VAL FROM QSYS2.QSQPTABL'; + $value = $this->fetchOne($sql); + return $value; + } + + if (strtoupper($idType) === 'S') { + //check i5_lib option + $sequenceName = $objectName; + return $this->lastSequenceId($sequenceName); + } + + //returns last identity value for the specified table + //if (strtoupper($idType) === 'I') { + $tableName = $objectName; + return $this->fetchOne('SELECT IDENTITY_VAL_LOCAL() from ' . $this->quoteIdentifier($tableName)); + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterException.php b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterException.php new file mode 100644 index 0000000..67e000d --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterException.php @@ -0,0 +1,50 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Exception; + +use gipfl\ZfDb\Exception\DbException; +use Exception; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class AdapterException extends DbException +{ + protected $_chainedException = null; + + public function __construct($message = '', $code = 0, Exception $e = null) + { + if ($e && (0 === $code)) { + $code = $e->getCode(); + $code = ctype_digit($code) ? (int) $code : 0; + } + parent::__construct($message, $code, $e); + } + + public function hasChainedException() + { + return ($this->getPrevious() !== null); + } + + public function getChainedException() + { + return $this->getPrevious(); + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionDb2.php b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionDb2.php new file mode 100644 index 0000000..c43b36f --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionDb2.php @@ -0,0 +1,42 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Exception; + +use Exception; + +/** + * Zend_Db_Adapter_Exception + */ + +/** + * Zend_Db_Adapter_Db2_Exception + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class AdapterExceptionDb2 extends AdapterException +{ + protected $code = '00000'; + protected $message = 'unknown exception'; + + public function __construct($message = 'unknown exception', $code = '00000', Exception $e = null) + { + parent::__construct($message, $code, $e); + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionMysqli.php b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionMysqli.php new file mode 100644 index 0000000..99029b4 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionMysqli.php @@ -0,0 +1,30 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + * + */ +namespace gipfl\ZfDb\Adapter\Exception; + +/** + * Zend_Db_Adapter_Mysqli_Exception + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class AdapterExceptionMysqli extends AdapterException +{ +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionOracle.php b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionOracle.php new file mode 100644 index 0000000..2d8f45d --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionOracle.php @@ -0,0 +1,51 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Exception; + +/** + * Zend_Db_Adapter_Oracle_Exception + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class AdapterExceptionOracle extends AdapterException +{ + protected $message = 'Unknown exception'; + protected $code = 0; + + public function __construct($error = null, $code = 0) + { + if (is_array($error)) { + if (!isset($error['offset'])) { + $this->message = $error['code'] .' '. $error['message']; + } else { + $this->message = $error['code'] .' '. $error['message']." " + . substr($error['sqltext'], 0, $error['offset']) + . "*" + . substr($error['sqltext'], $error['offset']); + } + $this->code = $error['code']; + } elseif (is_string($error)) { + $this->message = $error; + } + if (!$this->code && $code) { + $this->code = $code; + } + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionSqlsrv.php b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionSqlsrv.php new file mode 100644 index 0000000..dde821a --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Exception/AdapterExceptionSqlsrv.php @@ -0,0 +1,55 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Exception; + +use Exception; + +/** + * Zend_Db_Adapter_Sqlsrv_Exception + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class AdapterExceptionSqlsrv extends AdapterException +{ + /** + * Constructor + * + * If $message is an array, the assumption is that the return value of + * sqlsrv_errors() was provided. If so, it then retrieves the most recent + * error from that stack, and sets the message and code based on it. + * + * @param null|array|string $message + * @param null|int $code + */ + public function __construct($message = null, $code = 0) + { + if (is_array($message)) { + // Error should be array of errors + // We only need first one (?) + if (isset($message[0])) { + $message = $message[0]; + } + + $code = (int) $message['code']; + $message = (string) $message['message']; + } + parent::__construct($message, $code, new Exception($message, $code)); + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Mysqli.php b/vendor/gipfl/zfdb/src/Adapter/Mysqli.php new file mode 100644 index 0000000..15a191f --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Mysqli.php @@ -0,0 +1,499 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter; + +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Statement\Exception\StatementExceptionMysqli; +use gipfl\ZfDb\Statement\MysqliStatement; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Mysqli extends Adapter +{ + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INT' => Db::INT_TYPE, + 'INTEGER' => Db::INT_TYPE, + 'MEDIUMINT' => Db::INT_TYPE, + 'SMALLINT' => Db::INT_TYPE, + 'TINYINT' => Db::INT_TYPE, + 'BIGINT' => Db::BIGINT_TYPE, + 'SERIAL' => Db::BIGINT_TYPE, + 'DEC' => Db::FLOAT_TYPE, + 'DECIMAL' => Db::FLOAT_TYPE, + 'DOUBLE' => Db::FLOAT_TYPE, + 'DOUBLE PRECISION' => Db::FLOAT_TYPE, + 'FIXED' => Db::FLOAT_TYPE, + 'FLOAT' => Db::FLOAT_TYPE + ); + + /** + * @var MysqliStatement + */ + protected $_stmt = null; + + /** + * Default class name for a DB statement. + * + * @var string + */ + protected $_defaultStmtClass = MysqliStatement::class; + + /** + * Quote a raw string. + * + * @param mixed $value Raw string + * + * @return string Quoted string + */ + protected function _quote($value) + { + if (is_int($value) || is_float($value)) { + return $value; + } + $this->_connect(); + return "'" . $this->_connection->real_escape_string($value) . "'"; + } + + /** + * Returns the symbol the adapter uses for delimiting identifiers. + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return "`"; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + * @throws StatementExceptionMysqli + */ + public function listTables() + { + $result = array(); + // Use mysqli extension API, because SHOW doesn't work + // well as a prepared statement on MySQL 4.1. + $sql = 'SHOW TABLES'; + if ($queryResult = $this->getConnection()->query($sql)) { + while ($row = $queryResult->fetch_row()) { + $result[] = $row[0]; + } + $queryResult->close(); + } else { + throw new StatementExceptionMysqli($this->getConnection()->error); + } + return $result; + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + /** + * @todo use INFORMATION_SCHEMA someday when + * MySQL's implementation isn't too slow. + */ + + if ($schemaName) { + $sql = 'DESCRIBE ' . $this->quoteIdentifier("$schemaName.$tableName", true); + } else { + $sql = 'DESCRIBE ' . $this->quoteIdentifier($tableName, true); + } + + /** + * Use mysqli extension API, because DESCRIBE doesn't work + * well as a prepared statement on MySQL 4.1. + */ + if ($queryResult = $this->getConnection()->query($sql)) { + while ($row = $queryResult->fetch_assoc()) { + $result[] = $row; + } + $queryResult->close(); + } else { + throw new StatementExceptionMysqli($this->getConnection()->error); + } + + $desc = array(); + + $row_defaults = array( + 'Length' => null, + 'Scale' => null, + 'Precision' => null, + 'Unsigned' => null, + 'Primary' => false, + 'PrimaryPosition' => null, + 'Identity' => false + ); + $i = 1; + $p = 1; + foreach ($result as $key => $row) { + $row = array_merge($row_defaults, $row); + if (preg_match('/unsigned/', $row['Type'])) { + $row['Unsigned'] = true; + } + if (preg_match('/^((?:var)?char)\((\d+)\)/', $row['Type'], $matches)) { + $row['Type'] = $matches[1]; + $row['Length'] = $matches[2]; + } elseif (preg_match('/^decimal\((\d+),(\d+)\)/', $row['Type'], $matches)) { + $row['Type'] = 'decimal'; + $row['Precision'] = $matches[1]; + $row['Scale'] = $matches[2]; + } elseif (preg_match('/^float\((\d+),(\d+)\)/', $row['Type'], $matches)) { + $row['Type'] = 'float'; + $row['Precision'] = $matches[1]; + $row['Scale'] = $matches[2]; + } elseif (preg_match('/^((?:big|medium|small|tiny)?int)\((\d+)\)/', $row['Type'], $matches)) { + $row['Type'] = $matches[1]; + /** + * The optional argument of a MySQL int type is not precision + * or length; it is only a hint for display width. + */ + } + if (strtoupper($row['Key']) == 'PRI') { + $row['Primary'] = true; + $row['PrimaryPosition'] = $p; + if ($row['Extra'] == 'auto_increment') { + $row['Identity'] = true; + } else { + $row['Identity'] = false; + } + ++$p; + } + $desc[$this->foldCase($row['Field'])] = array( + 'SCHEMA_NAME' => null, // @todo + 'TABLE_NAME' => $this->foldCase($tableName), + 'COLUMN_NAME' => $this->foldCase($row['Field']), + 'COLUMN_POSITION' => $i, + 'DATA_TYPE' => $row['Type'], + 'DEFAULT' => $row['Default'], + 'NULLABLE' => (bool) ($row['Null'] == 'YES'), + 'LENGTH' => $row['Length'], + 'SCALE' => $row['Scale'], + 'PRECISION' => $row['Precision'], + 'UNSIGNED' => $row['Unsigned'], + 'PRIMARY' => $row['Primary'], + 'PRIMARY_POSITION' => $row['PrimaryPosition'], + 'IDENTITY' => $row['Identity'] + ); + ++$i; + } + return $desc; + } + + /** + * Creates a connection to the database. + * + * @return void + * @throws StatementExceptionMysqli + */ + protected function _connect() + { + if ($this->_connection) { + return; + } + + if (!extension_loaded('mysqli')) { + throw new StatementExceptionMysqli( + 'The Mysqli extension is required for this adapter but the extension is not loaded' + ); + } + + if (isset($this->_config['port'])) { + $port = (integer) $this->_config['port']; + } else { + $port = null; + } + + if (isset($this->_config['socket'])) { + $socket = $this->_config['socket']; + } else { + $socket = null; + } + + $this->_connection = mysqli_init(); + + if (!empty($this->_config['driver_options'])) { + foreach ($this->_config['driver_options'] as $option => $value) { + if (is_string($option)) { + // Suppress warnings here + // Ignore it if it's not a valid constant + $option = @constant(strtoupper($option)); + if ($option === null) { + continue; + } + } + mysqli_options($this->_connection, $option, $value); + } + } + + // Suppress connection warnings here. + // Throw an exception instead. + $_isConnected = @mysqli_real_connect( + $this->_connection, + $this->_config['host'], + $this->_config['username'], + $this->_config['password'], + $this->_config['dbname'], + $port, + $socket + ); + + if ($_isConnected === false || mysqli_connect_errno()) { + $this->closeConnection(); + throw new StatementExceptionMysqli(mysqli_connect_error()); + } + + if (!empty($this->_config['charset'])) { + mysqli_set_charset($this->_connection, $this->_config['charset']); + } + } + + /** + * Test if a connection is active + * + * @return boolean + */ + public function isConnected() + { + return ((bool) ($this->_connection instanceof mysqli)); + } + + /** + * Force the connection to close. + * + * @return void + */ + public function closeConnection() + { + if ($this->isConnected()) { + $this->_connection->close(); + } + $this->_connection = null; + } + + /** + * Prepare a statement and return a PDOStatement-like object. + * + * @param string $sql SQL query + * @return MysqliStatement|bool + */ + public function prepare($sql) + { + $this->_connect(); + if ($this->_stmt) { + $this->_stmt->close(); + } + $stmtClass = $this->_defaultStmtClass; + $stmt = new $stmtClass($this, $sql); + if ($stmt === false) { + return false; + } + $stmt->setFetchMode($this->_fetchMode); + $this->_stmt = $stmt; + return $stmt; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * MySQL does not support sequences, so $tableName and $primaryKey are ignored. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + * @todo Return value should be int? + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + $mysqli = $this->_connection; + return (string) $mysqli->insert_id; + } + + /** + * Begin a transaction. + * + * @return void + */ + protected function _beginTransaction() + { + $this->_connect(); + $this->_connection->autocommit(false); + } + + /** + * Commit a transaction. + * + * @return void + */ + protected function _commit() + { + $this->_connect(); + $this->_connection->commit(); + $this->_connection->autocommit(true); + } + + /** + * Roll-back a transaction. + * + * @return void + */ + protected function _rollBack() + { + $this->_connect(); + $this->_connection->rollback(); + $this->_connection->autocommit(true); + } + + /** + * Set the fetch mode. + * + * @param int $mode + * @return void + * @throws StatementExceptionMysqli + */ + public function setFetchMode($mode) + { + switch ($mode) { + case Db::FETCH_LAZY: + case Db::FETCH_ASSOC: + case Db::FETCH_NUM: + case Db::FETCH_BOTH: + case Db::FETCH_NAMED: + case Db::FETCH_OBJ: + $this->_fetchMode = $mode; + break; + case Db::FETCH_BOUND: // bound to PHP variable + throw new StatementExceptionMysqli('FETCH_BOUND is not supported yet'); + default: + throw new StatementExceptionMysqli("Invalid fetch mode '$mode' specified"); + } + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param int $count + * @param int $offset OPTIONAL + * @return string + * @throws StatementExceptionMysqli + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new StatementExceptionMysqli("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new StatementExceptionMysqli("LIMIT argument offset=$offset is not valid"); + } + + $sql .= " LIMIT $count"; + if ($offset > 0) { + $sql .= " OFFSET $offset"; + } + + return $sql; + } + + /** + * Check if the adapter supports real SQL parameters. + * + * @param string $type 'positional' or 'named' + * @return bool + */ + public function supportsParameters($type) + { + switch ($type) { + case 'positional': + return true; + case 'named': + default: + return false; + } + } + + /** + * Retrieve server version in PHP style + * + *@return string + */ + public function getServerVersion() + { + $this->_connect(); + $version = $this->_connection->server_version; + $major = (int) ($version / 10000); + $minor = (int) ($version % 10000 / 100); + $revision = (int) ($version % 100); + return $major . '.' . $minor . '.' . $revision; + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Oracle.php b/vendor/gipfl/zfdb/src/Adapter/Oracle.php new file mode 100644 index 0000000..3430834 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Oracle.php @@ -0,0 +1,595 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter; + +use gipfl\ZfDb\Adapter\Exception\AdapterExceptionOracle; +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Expr; +use gipfl\ZfDb\Statement\OracleStatement; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Oracle extends Adapter +{ + /** + * User-provided configuration. + * + * Basic keys are: + * + * username => (string) Connect to the database as this username. + * password => (string) Password associated with the username. + * dbname => Either the name of the local Oracle instance, or the + * name of the entry in tnsnames.ora to which you want to connect. + * persistent => (boolean) Set TRUE to use a persistent connection + * @var array + */ + protected $_config = array( + 'dbname' => null, + 'username' => null, + 'password' => null, + 'persistent' => false + ); + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'BINARY_DOUBLE' => Db::FLOAT_TYPE, + 'BINARY_FLOAT' => Db::FLOAT_TYPE, + 'NUMBER' => Db::FLOAT_TYPE, + ); + + /** + * @var integer + */ + protected $_execute_mode = null; + + /** + * Default class name for a DB statement. + * + * @var string + */ + protected $_defaultStmtClass = OracleStatement::class; + + /** + * Check if LOB field are returned as string + * instead of OCI-Lob object + * + * @var boolean + */ + protected $_lobAsString = null; + + /** + * Creates a connection resource. + * + * @return void + * @throws AdapterExceptionOracle + */ + protected function _connect() + { + if (is_resource($this->_connection)) { + // connection already exists + return; + } + + if (!extension_loaded('oci8')) { + throw new AdapterExceptionOracle( + 'The OCI8 extension is required for this adapter but the extension is not loaded' + ); + } + + $this->_setExecuteMode(OCI_COMMIT_ON_SUCCESS); + + $connectionFuncName = ($this->_config['persistent'] == true) ? 'oci_pconnect' : 'oci_connect'; + + $this->_connection = @$connectionFuncName( + $this->_config['username'], + $this->_config['password'], + $this->_config['dbname'], + $this->_config['charset'] + ); + + // check the connection + if (!$this->_connection) { + throw new AdapterExceptionOracle(oci_error()); + } + } + + /** + * Test if a connection is active + * + * @return boolean + */ + public function isConnected() + { + return ((bool) (is_resource($this->_connection) + && (get_resource_type($this->_connection) == 'oci8 connection' + || get_resource_type($this->_connection) == 'oci8 persistent connection'))); + } + + /** + * Force the connection to close. + * + * @return void + */ + public function closeConnection() + { + if ($this->isConnected()) { + oci_close($this->_connection); + } + $this->_connection = null; + } + + /** + * Activate/deactivate return of LOB as string + * + * @param string $lobAsString + * @return Oracle + */ + public function setLobAsString($lobAsString) + { + $this->_lobAsString = (bool) $lobAsString; + return $this; + } + + /** + * Return whether or not LOB are returned as string + * + * @return boolean + */ + public function getLobAsString() + { + if ($this->_lobAsString === null) { + // if never set by user, we use driver option if it exists otherwise false + if (isset($this->_config['driver_options']) && + isset($this->_config['driver_options']['lob_as_string'])) { + $this->_lobAsString = (bool) $this->_config['driver_options']['lob_as_string']; + } else { + $this->_lobAsString = false; + } + } + return $this->_lobAsString; + } + + /** + * Returns an SQL statement for preparation. + * + * @param string $sql The SQL statement with placeholders. + * @return OracleStatement + */ + public function prepare($sql) + { + $this->_connect(); + $stmtClass = $this->_defaultStmtClass; + $stmt = new $stmtClass($this, $sql); + if ($stmt instanceof OracleStatement) { + $stmt->setLobAsString($this->getLobAsString()); + } + $stmt->setFetchMode($this->_fetchMode); + return $stmt; + } + + /** + * Quote a raw string. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (is_int($value) || is_float($value)) { + return $value; + } + $value = str_replace("'", "''", $value); + return "'" . addcslashes($value, "\000\n\r\\\032") . "'"; + } + + /** + * Quote a table identifier and alias. + * + * @param string|array|Expr $ident The identifier or expression. + * @param string $alias An alias for the table. + * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option. + * @return string The quoted identifier and alias. + */ + public function quoteTableAs($ident, $alias = null, $auto = false) + { + // Oracle doesn't allow the 'AS' keyword between the table identifier/expression and alias. + return $this->_quoteIdentifierAs($ident, $alias, $auto, ' '); + } + + /** + * Return the most recent value from the specified sequence in the database. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function lastSequenceId($sequenceName) + { + $this->_connect(); + $sql = 'SELECT '.$this->quoteIdentifier($sequenceName, true).'.CURRVAL FROM dual'; + $value = $this->fetchOne($sql); + return $value; + } + + /** + * Generate a new value from the specified sequence in the database, and return it. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function nextSequenceId($sequenceName) + { + $this->_connect(); + $sql = 'SELECT '.$this->quoteIdentifier($sequenceName, true).'.NEXTVAL FROM dual'; + $value = $this->fetchOne($sql); + return $value; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * Oracle does not support IDENTITY columns, so if the sequence is not + * specified, this method returns null. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + if ($tableName !== null) { + $sequenceName = $tableName; + if ($primaryKey) { + $sequenceName .= "_$primaryKey"; + } + $sequenceName .= '_seq'; + return $this->lastSequenceId($sequenceName); + } + + // No support for IDENTITY columns; return null + return null; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $this->_connect(); + $data = $this->fetchCol('SELECT table_name FROM all_tables'); + return $data; + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @todo Discover integer unsigned property. + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + $version = $this->getServerVersion(); + if (($version === null) || version_compare($version, '9.0.0', '>=')) { + $sql = "SELECT TC.TABLE_NAME, TC.OWNER, TC.COLUMN_NAME, TC.DATA_TYPE, + TC.DATA_DEFAULT, TC.NULLABLE, TC.COLUMN_ID, TC.DATA_LENGTH, + TC.DATA_SCALE, TC.DATA_PRECISION, C.CONSTRAINT_TYPE, CC.POSITION + FROM ALL_TAB_COLUMNS TC + LEFT JOIN (ALL_CONS_COLUMNS CC JOIN ALL_CONSTRAINTS C + ON (CC.CONSTRAINT_NAME = C.CONSTRAINT_NAME AND CC.TABLE_NAME = C.TABLE_NAME + AND CC.OWNER = C.OWNER AND C.CONSTRAINT_TYPE = 'P')) + ON TC.TABLE_NAME = CC.TABLE_NAME AND TC.COLUMN_NAME = CC.COLUMN_NAME + WHERE UPPER(TC.TABLE_NAME) = UPPER(:TBNAME)"; + $bind[':TBNAME'] = $tableName; + if ($schemaName) { + $sql .= ' AND UPPER(TC.OWNER) = UPPER(:SCNAME)'; + $bind[':SCNAME'] = $schemaName; + } + $sql .= ' ORDER BY TC.COLUMN_ID'; + } else { + $subSql="SELECT AC.OWNER, AC.TABLE_NAME, ACC.COLUMN_NAME, AC.CONSTRAINT_TYPE, ACC.POSITION + from ALL_CONSTRAINTS AC, ALL_CONS_COLUMNS ACC + WHERE ACC.CONSTRAINT_NAME = AC.CONSTRAINT_NAME + AND ACC.TABLE_NAME = AC.TABLE_NAME + AND ACC.OWNER = AC.OWNER + AND AC.CONSTRAINT_TYPE = 'P' + AND UPPER(AC.TABLE_NAME) = UPPER(:TBNAME)"; + $bind[':TBNAME'] = $tableName; + if ($schemaName) { + $subSql .= ' AND UPPER(ACC.OWNER) = UPPER(:SCNAME)'; + $bind[':SCNAME'] = $schemaName; + } + $sql="SELECT TC.TABLE_NAME, TC.OWNER, TC.COLUMN_NAME, TC.DATA_TYPE, + TC.DATA_DEFAULT, TC.NULLABLE, TC.COLUMN_ID, TC.DATA_LENGTH, + TC.DATA_SCALE, TC.DATA_PRECISION, CC.CONSTRAINT_TYPE, CC.POSITION + FROM ALL_TAB_COLUMNS TC, ($subSql) CC + WHERE UPPER(TC.TABLE_NAME) = UPPER(:TBNAME) + AND TC.OWNER = CC.OWNER(+) AND TC.TABLE_NAME = CC.TABLE_NAME(+) + AND TC.COLUMN_NAME = CC.COLUMN_NAME(+)"; + if ($schemaName) { + $sql .= ' AND UPPER(TC.OWNER) = UPPER(:SCNAME)'; + } + $sql .= ' ORDER BY TC.COLUMN_ID'; + } + + $stmt = $this->query($sql, $bind); + + /** + * Use FETCH_NUM so we are not dependent on the CASE attribute of the PDO connection + */ + $result = $stmt->fetchAll(Db::FETCH_NUM); + + $table_name = 0; + $owner = 1; + $column_name = 2; + $data_type = 3; + $data_default = 4; + $nullable = 5; + $column_id = 6; + $data_length = 7; + $data_scale = 8; + $data_precision = 9; + $constraint_type = 10; + $position = 11; + + $desc = array(); + foreach ($result as $key => $row) { + list ($primary, $primaryPosition, $identity) = array(false, null, false); + if ($row[$constraint_type] == 'P') { + $primary = true; + $primaryPosition = $row[$position]; + /** + * Oracle does not support auto-increment keys. + */ + $identity = false; + } + $desc[$this->foldCase($row[$column_name])] = array( + 'SCHEMA_NAME' => $this->foldCase($row[$owner]), + 'TABLE_NAME' => $this->foldCase($row[$table_name]), + 'COLUMN_NAME' => $this->foldCase($row[$column_name]), + 'COLUMN_POSITION' => $row[$column_id], + 'DATA_TYPE' => $row[$data_type], + 'DEFAULT' => $row[$data_default], + 'NULLABLE' => (bool) ($row[$nullable] == 'Y'), + 'LENGTH' => $row[$data_length], + 'SCALE' => $row[$data_scale], + 'PRECISION' => $row[$data_precision], + 'UNSIGNED' => null, // @todo + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + return $desc; + } + + /** + * Leave autocommit mode and begin a transaction. + * + * @return void + */ + protected function _beginTransaction() + { + $this->_setExecuteMode(OCI_DEFAULT); + } + + /** + * Commit a transaction and return to autocommit mode. + * + * @return void + * @throws AdapterExceptionOracle + */ + protected function _commit() + { + if (!oci_commit($this->_connection)) { + throw new AdapterExceptionOracle(oci_error($this->_connection)); + } + $this->_setExecuteMode(OCI_COMMIT_ON_SUCCESS); + } + + /** + * Roll back a transaction and return to autocommit mode. + * + * @return void + * @throws AdapterExceptionOracle + */ + protected function _rollBack() + { + if (!oci_rollback($this->_connection)) { + throw new AdapterExceptionOracle(oci_error($this->_connection)); + } + $this->_setExecuteMode(OCI_COMMIT_ON_SUCCESS); + } + + /** + * Set the fetch mode. + * + * @param integer $mode A fetch mode. + * @return void + * @throws AdapterExceptionOracle + *@todo Support FETCH_CLASS and FETCH_INTO. + * + */ + public function setFetchMode($mode) + { + switch ($mode) { + case Db::FETCH_NUM: // seq array + case Db::FETCH_ASSOC: // assoc array + case Db::FETCH_BOTH: // seq+assoc array + case Db::FETCH_OBJ: // object + $this->_fetchMode = $mode; + break; + case Db::FETCH_BOUND: // bound to PHP variable + throw new AdapterExceptionOracle('FETCH_BOUND is not supported yet'); + default: + throw new AdapterExceptionOracle("Invalid fetch mode '$mode' specified"); + } + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @return string + * @throws AdapterExceptionOracle + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterExceptionOracle("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterExceptionOracle("LIMIT argument offset=$offset is not valid"); + } + + /** + * Oracle does not implement the LIMIT clause as some RDBMS do. + * We have to simulate it with subqueries and ROWNUM. + * Unfortunately because we use the column wildcard "*", + * this puts an extra column into the query result set. + */ + $limit_sql = "SELECT z2.* + FROM ( + SELECT z1.*, ROWNUM AS \"zend_db_rownum\" + FROM ( + " . $sql . " + ) z1 + ) z2 + WHERE z2.\"zend_db_rownum\" BETWEEN " . ($offset+1) . " AND " . ($offset+$count); + return $limit_sql; + } + + /** + * @param integer $mode + * @throws AdapterExceptionOracle + */ + private function _setExecuteMode($mode) + { + switch ($mode) { + case OCI_COMMIT_ON_SUCCESS: + case OCI_DEFAULT: + case OCI_DESCRIBE_ONLY: + $this->_execute_mode = $mode; + break; + default: + throw new AdapterExceptionOracle("Invalid execution mode '$mode' specified"); + } + } + + /** + * @return int + */ + public function _getExecuteMode() + { + return $this->_execute_mode; + } + + /** + * Check if the adapter supports real SQL parameters. + * + * @param string $type 'positional' or 'named' + * @return bool + */ + public function supportsParameters($type) + { + switch ($type) { + case 'named': + return true; + case 'positional': + default: + return false; + } + } + + /** + * Retrieve server version in PHP style + * + * @return string + */ + public function getServerVersion() + { + $this->_connect(); + $version = oci_server_version($this->_connection); + if ($version !== false) { + $matches = null; + if (preg_match('/((?:[0-9]{1,2}\.){1,3}[0-9]{1,2})/', $version, $matches)) { + return $matches[1]; + } else { + return null; + } + } else { + return null; + } + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm.php new file mode 100644 index 0000000..c953349 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm.php @@ -0,0 +1,352 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Adapter\Pdo\Ibm\Db2; +use gipfl\ZfDb\Adapter\Pdo\Ibm\Ids; +use gipfl\ZfDb\Db; +use PDO; +use PDOException; +use PDOStatement; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Ibm extends PdoAdapter +{ + /** + * PDO type. + * + * @var string + */ + protected $_pdoType = 'ibm'; + + /** + * The IBM data server connected to + * + * @var Db2|Ids + */ + protected $_serverType = null; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INTEGER' => Db::INT_TYPE, + 'SMALLINT' => Db::INT_TYPE, + 'BIGINT' => Db::BIGINT_TYPE, + 'DECIMAL' => Db::FLOAT_TYPE, + 'DEC' => Db::FLOAT_TYPE, + 'REAL' => Db::FLOAT_TYPE, + 'NUMERIC' => Db::FLOAT_TYPE, + 'DOUBLE PRECISION' => Db::FLOAT_TYPE, + 'FLOAT' => Db::FLOAT_TYPE + ); + + /** + * Creates a PDO object and connects to the database. + * + * The IBM data server is set. + * Current options are DB2 or IDS + * @todo also differentiate between z/OS and i/5 + * + * @return void + * @throws AdapterException + */ + public function _connect() + { + if ($this->_connection) { + return; + } + parent::_connect(); + + $this->getConnection()->setAttribute(Db::ATTR_STRINGIFY_FETCHES, true); + + try { + if ($this->_serverType === null) { + $server = substr($this->getConnection()->getAttribute(PDO::ATTR_SERVER_INFO), 0, 3); + + switch ($server) { + case 'DB2': + $this->_serverType = new Db2($this); + + // Add DB2-specific numeric types + $this->_numericDataTypes['DECFLOAT'] = Db::FLOAT_TYPE; + $this->_numericDataTypes['DOUBLE'] = Db::FLOAT_TYPE; + $this->_numericDataTypes['NUM'] = Db::FLOAT_TYPE; + + break; + case 'IDS': + $this->_serverType = new Ids($this); + + // Add IDS-specific numeric types + $this->_numericDataTypes['SERIAL'] = Db::INT_TYPE; + $this->_numericDataTypes['SERIAL8'] = Db::BIGINT_TYPE; + $this->_numericDataTypes['INT8'] = Db::BIGINT_TYPE; + $this->_numericDataTypes['SMALLFLOAT'] = Db::FLOAT_TYPE; + $this->_numericDataTypes['MONEY'] = Db::FLOAT_TYPE; + + break; + } + } + } catch (PDOException $e) { + /** @see AdapterException */ + $error = strpos($e->getMessage(), 'driver does not support that attribute'); + if ($error) { + throw new AdapterException( + "PDO_IBM driver extension is downlevel. Please use driver release version 1.2.1 or later", + 0, + $e + ); + } else { + throw new AdapterException($e->getMessage(), $e->getCode(), $e); + } + } + } + + /** + * Creates a PDO DSN for the adapter from $this->_config settings. + * + * @return string + */ + protected function _dsn() + { + $this->_checkRequiredOptions($this->_config); + + // check if using full connection string + if (array_key_exists('host', $this->_config)) { + $dsn = ';DATABASE=' . $this->_config['dbname'] + . ';HOSTNAME=' . $this->_config['host'] + . ';PORT=' . $this->_config['port'] + // PDO_IBM supports only DB2 TCPIP protocol + . ';PROTOCOL=' . 'TCPIP;'; + } else { + // catalogued connection + $dsn = $this->_config['dbname']; + } + return $this->_pdoType . ': ' . $dsn; + } + + /** + * Checks required options + * + * @param array $config + * @throws AdapterException + * @return void + */ + protected function _checkRequiredOptions(array $config) + { + parent::_checkRequiredOptions($config); + + if (array_key_exists('host', $this->_config) && + !array_key_exists('port', $config)) { + throw new AdapterException("Configuration must have a key for 'port' when 'host' is specified"); + } + } + + /** + * Prepares an SQL statement. + * + * @param string $sql The SQL statement with placeholders. + * @param array $bind An array of data to bind to the placeholders. + * @return PDOStatement + */ + public function prepare($sql) + { + $this->_connect(); + $stmtClass = $this->_defaultStmtClass; + $stmt = new $stmtClass($this, $sql); + $stmt->setFetchMode($this->_fetchMode); + return $stmt; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $this->_connect(); + return $this->_serverType->listTables(); + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * + * @todo Discover integer unsigned property. + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + $this->_connect(); + return $this->_serverType->describeTable($tableName, $schemaName); + } + + /** + * Inserts a table row with specified data. + * Special handling for PDO_IBM + * remove empty slots + * + * @param mixed $table The table to insert data into. + * @param array $bind Column-value pairs. + * @return int The number of affected rows. + */ + public function insert($table, array $bind) + { + $this->_connect(); + $newbind = array(); + if (is_array($bind)) { + foreach ($bind as $name => $value) { + if ($value !== null) { + $newbind[$name] = $value; + } + } + } + + return parent::insert($table, $newbind); + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $this->_connect(); + return $this->_serverType->limit($sql, $count, $offset); + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT + * column. + * + * @param string $tableName OPTIONAL + * @param string $primaryKey OPTIONAL + * @return integer + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + $this->_connect(); + + if ($tableName !== null) { + $sequenceName = $tableName; + if ($primaryKey) { + $sequenceName .= "_$primaryKey"; + } + $sequenceName .= '_seq'; + return $this->lastSequenceId($sequenceName); + } + + $id = $this->getConnection()->lastInsertId(); + + return $id; + } + + /** + * Return the most recent value from the specified sequence in the database. + * + * @param string $sequenceName + * @return integer + */ + public function lastSequenceId($sequenceName) + { + $this->_connect(); + return $this->_serverType->lastSequenceId($sequenceName); + } + + /** + * Generate a new value from the specified sequence in the database, + * and return it. + * + * @param string $sequenceName + * @return integer + */ + public function nextSequenceId($sequenceName) + { + $this->_connect(); + return $this->_serverType->nextSequenceId($sequenceName); + } + + /** + * Retrieve server version in PHP style + * Pdo_Idm doesn't support getAttribute(PDO::ATTR_SERVER_VERSION) + * @return string + */ + public function getServerVersion() + { + try { + $stmt = $this->query( + 'SELECT service_level, fixpack_num FROM TABLE (sysproc.env_get_inst_info()) as INSTANCEINFO' + ); + $result = $stmt->fetchAll(Db::FETCH_NUM); + if (count($result)) { + $matches = null; + if (preg_match('/((?:[0-9]{1,2}\.){1,3}[0-9]{1,2})/', $result[0][0], $matches)) { + return $matches[1]; + } else { + return null; + } + } + return null; + } catch (PDOException $e) { + return null; + } + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm/Db2.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm/Db2.php new file mode 100644 index 0000000..7dea70a --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm/Db2.php @@ -0,0 +1,215 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo\Ibm; + +use gipfl\ZfDb\Adapter\Adapter; +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Db; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Db2 +{ + /** + * @var Adapter + */ + protected $_adapter = null; + + /** + * Construct the data server class. + * + * It will be used to generate non-generic SQL + * for a particular data server + * + * @param Adapter $adapter + */ + public function __construct($adapter) + { + $this->_adapter = $adapter; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $sql = "SELECT tabname " + . "FROM SYSCAT.TABLES "; + return $this->_adapter->fetchCol($sql); + } + + /** + * DB2 catalog lookup for describe table + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + $sql = "SELECT DISTINCT c.tabschema, c.tabname, c.colname, c.colno, + c.typename, c.default, c.nulls, c.length, c.scale, + c.identity, tc.type AS tabconsttype, k.colseq + FROM syscat.columns c + LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc + ON (k.tabschema = tc.tabschema + AND k.tabname = tc.tabname + AND tc.type = 'P')) + ON (c.tabschema = k.tabschema + AND c.tabname = k.tabname + AND c.colname = k.colname) + WHERE " + . $this->_adapter->quoteInto('UPPER(c.tabname) = UPPER(?)', $tableName); + if ($schemaName) { + $sql .= $this->_adapter->quoteInto(' AND UPPER(c.tabschema) = UPPER(?)', $schemaName); + } + $sql .= " ORDER BY c.colno"; + + $desc = array(); + $stmt = $this->_adapter->query($sql); + + /** + * To avoid case issues, fetch using FETCH_NUM + */ + $result = $stmt->fetchAll(Db::FETCH_NUM); + + /** + * The ordering of columns is defined by the query so we can map + * to variables to improve readability + */ + $tabschema = 0; + $tabname = 1; + $colname = 2; + $colno = 3; + $typename = 4; + $default = 5; + $nulls = 6; + $length = 7; + $scale = 8; + $identityCol = 9; + $tabconstype = 10; + $colseq = 11; + + foreach ($result as $key => $row) { + list ($primary, $primaryPosition, $identity) = array(false, null, false); + if ($row[$tabconstype] == 'P') { + $primary = true; + $primaryPosition = $row[$colseq]; + } + /** + * In IBM DB2, an column can be IDENTITY + * even if it is not part of the PRIMARY KEY. + */ + if ($row[$identityCol] == 'Y') { + $identity = true; + } + + $desc[$this->_adapter->foldCase($row[$colname])] = array( + 'SCHEMA_NAME' => $this->_adapter->foldCase($row[$tabschema]), + 'TABLE_NAME' => $this->_adapter->foldCase($row[$tabname]), + 'COLUMN_NAME' => $this->_adapter->foldCase($row[$colname]), + 'COLUMN_POSITION' => $row[$colno]+1, + 'DATA_TYPE' => $row[$typename], + 'DEFAULT' => $row[$default], + 'NULLABLE' => (bool) ($row[$nulls] == 'Y'), + 'LENGTH' => $row[$length], + 'SCALE' => $row[$scale], + 'PRECISION' => ($row[$typename] == 'DECIMAL' ? $row[$length] : 0), + 'UNSIGNED' => false, + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + + return $desc; + } + + /** + * Adds a DB2-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @throws AdapterException + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count < 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } else { + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + + if ($offset == 0 && $count > 0) { + $limit_sql = $sql . " FETCH FIRST $count ROWS ONLY"; + return $limit_sql; + } + /** + * DB2 does not implement the LIMIT clause as some RDBMS do. + * We have to simulate it with subqueries and ROWNUM. + * Unfortunately because we use the column wildcard "*", + * this puts an extra column into the query result set. + */ + $limit_sql = "SELECT z2.* + FROM ( + SELECT ROW_NUMBER() OVER() AS \"ZEND_DB_ROWNUM\", z1.* + FROM ( + " . $sql . " + ) z1 + ) z2 + WHERE z2.zend_db_rownum BETWEEN " . ($offset+1) . " AND " . ($offset+$count); + } + return $limit_sql; + } + + /** + * DB2-specific last sequence id + * + * @param string $sequenceName + * @return integer + */ + public function lastSequenceId($sequenceName) + { + $sql = 'SELECT PREVVAL FOR '.$this->_adapter->quoteIdentifier($sequenceName).' AS VAL FROM SYSIBM.SYSDUMMY1'; + $value = $this->_adapter->fetchOne($sql); + return $value; + } + + /** + * DB2-specific sequence id value + * + * @param string $sequenceName + * @return integer + */ + public function nextSequenceId($sequenceName) + { + $sql = 'SELECT NEXTVAL FOR '.$this->_adapter->quoteIdentifier($sequenceName).' AS VAL FROM SYSIBM.SYSDUMMY1'; + $value = $this->_adapter->fetchOne($sql); + return $value; + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm/Ids.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm/Ids.php new file mode 100644 index 0000000..730b3b2 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Ibm/Ids.php @@ -0,0 +1,290 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo\Ibm; + +use gipfl\ZfDb\Adapter\Adapter; +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Db; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Ids +{ + /** + * @var Adapter + */ + protected $_adapter = null; + + /** + * Construct the data server class. + * + * It will be used to generate non-generic SQL + * for a particular data server + * + * @param Adapter $adapter + */ + public function __construct($adapter) + { + $this->_adapter = $adapter; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $sql = "SELECT tabname " + . "FROM systables "; + + return $this->_adapter->fetchCol($sql); + } + + /** + * IDS catalog lookup for describe table + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + // this is still a work in progress + + $sql= "SELECT DISTINCT t.owner, t.tabname, c.colname, c.colno, c.coltype, + d.default, c.collength, t.tabid + FROM syscolumns c + JOIN systables t ON c.tabid = t.tabid + LEFT JOIN sysdefaults d ON c.tabid = d.tabid AND c.colno = d.colno + WHERE " + . $this->_adapter->quoteInto('UPPER(t.tabname) = UPPER(?)', $tableName); + if ($schemaName) { + $sql .= $this->_adapter->quoteInto(' AND UPPER(t.owner) = UPPER(?)', $schemaName); + } + $sql .= " ORDER BY c.colno"; + + $desc = array(); + $stmt = $this->_adapter->query($sql); + + $result = $stmt->fetchAll(Db::FETCH_NUM); + + /** + * The ordering of columns is defined by the query so we can map + * to variables to improve readability + */ + $tabschema = 0; + $tabname = 1; + $colname = 2; + $colno = 3; + $typename = 4; + $default = 5; + $length = 6; + $tabid = 7; + + $primaryCols = null; + + foreach ($result as $key => $row) { + $primary = false; + $primaryPosition = null; + + if (!$primaryCols) { + $primaryCols = $this->_getPrimaryInfo($row[$tabid]); + } + + if (array_key_exists($row[$colno], $primaryCols)) { + $primary = true; + $primaryPosition = $primaryCols[$row[$colno]]; + } + + $identity = false; + if ($row[$typename] == 6 + 256 || + $row[$typename] == 18 + 256) { + $identity = true; + } + + $desc[$this->_adapter->foldCase($row[$colname])] = array ( + 'SCHEMA_NAME' => $this->_adapter->foldCase($row[$tabschema]), + 'TABLE_NAME' => $this->_adapter->foldCase($row[$tabname]), + 'COLUMN_NAME' => $this->_adapter->foldCase($row[$colname]), + 'COLUMN_POSITION' => $row[$colno], + 'DATA_TYPE' => $this->_getDataType($row[$typename]), + 'DEFAULT' => $row[$default], + 'NULLABLE' => (bool) !($row[$typename] - 256 >= 0), + 'LENGTH' => $row[$length], + 'SCALE' => ($row[$typename] == 5 ? $row[$length]&255 : 0), + 'PRECISION' => ($row[$typename] == 5 ? (int)($row[$length]/256) : 0), + 'UNSIGNED' => false, + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + + return $desc; + } + + /** + * Map number representation of a data type + * to a string + * + * @param int $typeNo + * @return string + */ + protected function _getDataType($typeNo) + { + $typemap = array( + 0 => "CHAR", + 1 => "SMALLINT", + 2 => "INTEGER", + 3 => "FLOAT", + 4 => "SMALLFLOAT", + 5 => "DECIMAL", + 6 => "SERIAL", + 7 => "DATE", + 8 => "MONEY", + 9 => "NULL", + 10 => "DATETIME", + 11 => "BYTE", + 12 => "TEXT", + 13 => "VARCHAR", + 14 => "INTERVAL", + 15 => "NCHAR", + 16 => "NVARCHAR", + 17 => "INT8", + 18 => "SERIAL8", + 19 => "SET", + 20 => "MULTISET", + 21 => "LIST", + 22 => "Unnamed ROW", + 40 => "Variable-length opaque type", + 4118 => "Named ROW" + ); + + if ($typeNo - 256 >= 0) { + $typeNo = $typeNo - 256; + } + + return $typemap[$typeNo]; + } + + /** + * Helper method to retrieve primary key column + * and column location + * + * @param int $tabid + * @return array + */ + protected function _getPrimaryInfo($tabid) + { + $sql = "SELECT i.part1, i.part2, i.part3, i.part4, i.part5, i.part6, + i.part7, i.part8, i.part9, i.part10, i.part11, i.part12, + i.part13, i.part14, i.part15, i.part16 + FROM sysindexes i + JOIN sysconstraints c ON c.idxname = i.idxname + WHERE i.tabid = " . $tabid . " AND c.constrtype = 'P'"; + + $stmt = $this->_adapter->query($sql); + $results = $stmt->fetchAll(); + + $cols = array(); + + // this should return only 1 row + // unless there is no primary key, + // in which case, the empty array is returned + if ($results) { + $row = $results[0]; + } else { + return $cols; + } + + $position = 0; + foreach ($row as $key => $colno) { + $position++; + if ($colno == 0) { + return $cols; + } else { + $cols[$colno] = $position; + } + } + + return $cols; + } + + /** + * Adds an IDS-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @throws AdapterException + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count < 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } elseif ($count == 0) { + $limit_sql = str_ireplace("SELECT", "SELECT * FROM (SELECT", $sql); + $limit_sql .= ") WHERE 0 = 1"; + } else { + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + if ($offset == 0) { + $limit_sql = str_ireplace("SELECT", "SELECT FIRST $count", $sql); + } else { + $limit_sql = str_ireplace("SELECT", "SELECT SKIP $offset LIMIT $count", $sql); + } + } + return $limit_sql; + } + + /** + * IDS-specific last sequence id + * + * @param string $sequenceName + * @return integer + */ + public function lastSequenceId($sequenceName) + { + $sql = 'SELECT '.$this->_adapter->quoteIdentifier($sequenceName).'.CURRVAL FROM ' + .'systables WHERE tabid = 1'; + $value = $this->_adapter->fetchOne($sql); + return $value; + } + + /** + * IDS-specific sequence id value + * + * @param string $sequenceName + * @return integer + */ + public function nextSequenceId($sequenceName) + { + $sql = 'SELECT '.$this->_adapter->quoteIdentifier($sequenceName).'.NEXTVAL FROM ' + .'systables WHERE tabid = 1'; + $value = $this->_adapter->fetchOne($sql); + return $value; + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Mssql.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Mssql.php new file mode 100644 index 0000000..a75097c --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Mssql.php @@ -0,0 +1,384 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Db; +use PDOException; + +/** + * Class for connecting to Microsoft SQL Server databases and performing common operations. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Mssql extends PdoAdapter +{ + /** + * PDO type. + * + * @var string + */ + protected $_pdoType = 'mssql'; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INT' => Db::INT_TYPE, + 'SMALLINT' => Db::INT_TYPE, + 'TINYINT' => Db::INT_TYPE, + 'BIGINT' => Db::BIGINT_TYPE, + 'DECIMAL' => Db::FLOAT_TYPE, + 'FLOAT' => Db::FLOAT_TYPE, + 'MONEY' => Db::FLOAT_TYPE, + 'NUMERIC' => Db::FLOAT_TYPE, + 'REAL' => Db::FLOAT_TYPE, + 'SMALLMONEY' => Db::FLOAT_TYPE + ); + + /** + * Creates a PDO DSN for the adapter from $this->_config settings. + * + * @return string + */ + protected function _dsn() + { + // baseline of DSN parts + $dsn = $this->_config; + + // don't pass the username and password in the DSN + unset($dsn['username']); + unset($dsn['password']); + unset($dsn['options']); + unset($dsn['persistent']); + unset($dsn['driver_options']); + + if (isset($dsn['port'])) { + $seperator = ':'; + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $seperator = ','; + } + $dsn['host'] .= $seperator . $dsn['port']; + unset($dsn['port']); + } + + // this driver supports multiple DSN prefixes + // @see http://www.php.net/manual/en/ref.pdo-dblib.connection.php + if (isset($dsn['pdoType'])) { + switch (strtolower($dsn['pdoType'])) { + case 'freetds': + case 'sybase': + $this->_pdoType = 'sybase'; + break; + case 'mssql': + $this->_pdoType = 'mssql'; + break; + case 'dblib': + default: + $this->_pdoType = 'dblib'; + break; + } + unset($dsn['pdoType']); + } + + // use all remaining parts in the DSN + foreach ($dsn as $key => $val) { + $dsn[$key] = "$key=$val"; + } + + $dsn = $this->_pdoType . ':' . implode(';', $dsn); + return $dsn; + } + + /** + * @return void + */ + protected function _connect() + { + if ($this->_connection) { + return; + } + parent::_connect(); + $this->_connection->exec('SET QUOTED_IDENTIFIER ON'); + } + + /** + * Begin a transaction. + * + * It is necessary to override the abstract PDO transaction functions here, as + * the PDO driver for MSSQL does not support transactions. + */ + protected function _beginTransaction() + { + $this->_connect(); + $this->_connection->exec('BEGIN TRANSACTION'); + return true; + } + + /** + * Commit a transaction. + * + * It is necessary to override the abstract PDO transaction functions here, as + * the PDO driver for MSSQL does not support transactions. + */ + protected function _commit() + { + $this->_connect(); + $this->_connection->exec('COMMIT TRANSACTION'); + return true; + } + + /** + * Roll-back a transaction. + * + * It is necessary to override the abstract PDO transaction functions here, as + * the PDO driver for MSSQL does not support transactions. + */ + protected function _rollBack() + { + $this->_connect(); + $this->_connection->exec('ROLLBACK TRANSACTION'); + return true; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $sql = "SELECT name FROM sysobjects WHERE type = 'U' ORDER BY name"; + return $this->fetchCol($sql); + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * PRIMARY_AUTO => integer; position of auto-generated column in primary key + * + * @todo Discover column primary key position. + * @todo Discover integer unsigned property. + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + if ($schemaName != null) { + if (strpos($schemaName, '.') !== false) { + $result = explode('.', $schemaName); + $schemaName = $result[1]; + } + } + /** + * Discover metadata information about this table. + */ + $sql = "exec sp_columns @table_name = " . $this->quoteIdentifier($tableName, true); + if ($schemaName != null) { + $sql .= ", @table_owner = " . $this->quoteIdentifier($schemaName, true); + } + + $stmt = $this->query($sql); + $result = $stmt->fetchAll(Db::FETCH_NUM); + + $table_name = 2; + $column_name = 3; + $type_name = 5; + $precision = 6; + $length = 7; + $scale = 8; + $nullable = 10; + $column_def = 12; + $column_position = 16; + + /** + * Discover primary key column(s) for this table. + */ + $sql = "exec sp_pkeys @table_name = " . $this->quoteIdentifier($tableName, true); + if ($schemaName != null) { + $sql .= ", @table_owner = " . $this->quoteIdentifier($schemaName, true); + } + + $stmt = $this->query($sql); + $primaryKeysResult = $stmt->fetchAll(Db::FETCH_NUM); + $primaryKeyColumn = array(); + $pkey_column_name = 3; + $pkey_key_seq = 4; + foreach ($primaryKeysResult as $pkeysRow) { + $primaryKeyColumn[$pkeysRow[$pkey_column_name]] = $pkeysRow[$pkey_key_seq]; + } + + $desc = array(); + $p = 1; + foreach ($result as $key => $row) { + $identity = false; + $words = explode(' ', $row[$type_name], 2); + if (isset($words[0])) { + $type = $words[0]; + if (isset($words[1])) { + $identity = (bool) preg_match('/identity/', $words[1]); + } + } + + $isPrimary = array_key_exists($row[$column_name], $primaryKeyColumn); + if ($isPrimary) { + $primaryPosition = $primaryKeyColumn[$row[$column_name]]; + } else { + $primaryPosition = null; + } + + $desc[$this->foldCase($row[$column_name])] = array( + 'SCHEMA_NAME' => null, // @todo + 'TABLE_NAME' => $this->foldCase($row[$table_name]), + 'COLUMN_NAME' => $this->foldCase($row[$column_name]), + 'COLUMN_POSITION' => (int) $row[$column_position], + 'DATA_TYPE' => $type, + 'DEFAULT' => $row[$column_def], + 'NULLABLE' => (bool) $row[$nullable], + 'LENGTH' => $row[$length], + 'SCALE' => $row[$scale], + 'PRECISION' => $row[$precision], + 'UNSIGNED' => null, // @todo + 'PRIMARY' => $isPrimary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + return $desc; + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @link http://lists.bestpractical.com/pipermail/rt-devel/2005-June/007339.html + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @throws AdapterException + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + + $sql .= " OFFSET $offset ROWS FETCH NEXT $count ROWS ONLY"; + + return $sql; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * Microsoft SQL Server does not support sequences, so the arguments to + * this method are ignored. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + * @throws AdapterException + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + $sql = 'SELECT SCOPE_IDENTITY()'; + return (int)$this->fetchOne($sql); + } + + /** + * Retrieve server version in PHP style + * Pdo_Mssql doesn't support getAttribute(PDO::ATTR_SERVER_VERSION) + * @return string + */ + public function getServerVersion() + { + try { + $stmt = $this->query("SELECT CAST(SERVERPROPERTY('productversion') AS VARCHAR)"); + $result = $stmt->fetchAll(Db::FETCH_NUM); + if (count($result)) { + return $result[0][0]; + } + return null; + } catch (PDOException $e) { + return null; + } + } + + /** + * Quote a raw string. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (!is_int($value) && !is_float($value)) { + // Fix for null-byte injection + $value = addcslashes($value, "\000\032"); + } + return parent::_quote($value); + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Mysql.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Mysql.php new file mode 100644 index 0000000..a84ae84 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Mysql.php @@ -0,0 +1,258 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Db; + +/** + * Class for connecting to MySQL databases and performing common operations. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Mysql extends PdoAdapter +{ + + /** + * PDO type. + * + * @var string + */ + protected $_pdoType = 'mysql'; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INT' => Db::INT_TYPE, + 'INTEGER' => Db::INT_TYPE, + 'MEDIUMINT' => Db::INT_TYPE, + 'SMALLINT' => Db::INT_TYPE, + 'TINYINT' => Db::INT_TYPE, + 'BIGINT' => Db::BIGINT_TYPE, + 'SERIAL' => Db::BIGINT_TYPE, + 'DEC' => Db::FLOAT_TYPE, + 'DECIMAL' => Db::FLOAT_TYPE, + 'DOUBLE' => Db::FLOAT_TYPE, + 'DOUBLE PRECISION' => Db::FLOAT_TYPE, + 'FIXED' => Db::FLOAT_TYPE, + 'FLOAT' => Db::FLOAT_TYPE + ); + + /** + * Override _dsn() and ensure that charset is incorporated in mysql + * @see PdoAdapter::_dsn() + */ + protected function _dsn() + { + $dsn = parent::_dsn(); + if (isset($this->_config['charset'])) { + $dsn .= ';charset=' . $this->_config['charset']; + } + return $dsn; + } + + /** + * Creates a PDO object and connects to the database. + * + * @return void + * @throws AdapterException + */ + protected function _connect() + { + if ($this->_connection) { + return; + } + + if (!empty($this->_config['charset']) + && version_compare(PHP_VERSION, '5.3.6', '<') + ) { + $initCommand = "SET NAMES '" . $this->_config['charset'] . "'"; + $this->_config['driver_options'][1002] = $initCommand; // 1002 = PDO::MYSQL_ATTR_INIT_COMMAND + } + + parent::_connect(); + } + + /** + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return "`"; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + return $this->fetchCol('SHOW TABLES'); + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + // @todo use INFORMATION_SCHEMA someday when MySQL's + // implementation has reasonably good performance and + // the version with this improvement is in wide use. + + if ($schemaName) { + $sql = 'DESCRIBE ' . $this->quoteIdentifier("$schemaName.$tableName", true); + } else { + $sql = 'DESCRIBE ' . $this->quoteIdentifier($tableName, true); + } + $stmt = $this->query($sql); + + // Use FETCH_NUM so we are not dependent on the CASE attribute of the PDO connection + $result = $stmt->fetchAll(Db::FETCH_NUM); + + $field = 0; + $type = 1; + $null = 2; + $key = 3; + $default = 4; + $extra = 5; + + $desc = array(); + $i = 1; + $p = 1; + foreach ($result as $row) { + list($length, $scale, $precision, $unsigned, $primary, $primaryPosition, $identity) + = array(null, null, null, null, false, null, false); + if (preg_match('/unsigned/', $row[$type])) { + $unsigned = true; + } + if (preg_match('/^((?:var)?char)\((\d+)\)/', $row[$type], $matches)) { + $row[$type] = $matches[1]; + $length = $matches[2]; + } elseif (preg_match('/^decimal\((\d+),(\d+)\)/', $row[$type], $matches)) { + $row[$type] = 'decimal'; + $precision = $matches[1]; + $scale = $matches[2]; + } elseif (preg_match('/^float\((\d+),(\d+)\)/', $row[$type], $matches)) { + $row[$type] = 'float'; + $precision = $matches[1]; + $scale = $matches[2]; + } elseif (preg_match('/^((?:big|medium|small|tiny)?int)\((\d+)\)/', $row[$type], $matches)) { + $row[$type] = $matches[1]; + // The optional argument of a MySQL int type is not precision + // or length; it is only a hint for display width. + } + if (strtoupper($row[$key]) == 'PRI') { + $primary = true; + $primaryPosition = $p; + if ($row[$extra] == 'auto_increment') { + $identity = true; + } else { + $identity = false; + } + ++$p; + } + $desc[$this->foldCase($row[$field])] = array( + 'SCHEMA_NAME' => null, // @todo + 'TABLE_NAME' => $this->foldCase($tableName), + 'COLUMN_NAME' => $this->foldCase($row[$field]), + 'COLUMN_POSITION' => $i, + 'DATA_TYPE' => $row[$type], + 'DEFAULT' => $row[$default], + 'NULLABLE' => (bool) ($row[$null] == 'YES'), + 'LENGTH' => $length, + 'SCALE' => $scale, + 'PRECISION' => $precision, + 'UNSIGNED' => $unsigned, + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + ++$i; + } + return $desc; + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @throws AdapterException + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + + $sql .= " LIMIT $count"; + if ($offset > 0) { + $sql .= " OFFSET $offset"; + } + + return $sql; + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Oci.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Oci.php new file mode 100644 index 0000000..b76af36 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Oci.php @@ -0,0 +1,367 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Adapter\Exception\AdapterExceptionOracle; +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Expr; + +/** + * Class for connecting to Oracle databases and performing common operations. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Oci extends PdoAdapter +{ + /** + * PDO type. + * + * @var string + */ + protected $_pdoType = 'oci'; + + /** + * Default class name for a DB statement. + * + * @var string + */ + protected $_defaultStmtClass = 'Zend_Db_Statement_Pdo_Oci'; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'BINARY_DOUBLE' => Db::FLOAT_TYPE, + 'BINARY_FLOAT' => Db::FLOAT_TYPE, + 'NUMBER' => Db::FLOAT_TYPE + ); + + /** + * Creates a PDO DSN for the adapter from $this->_config settings. + * + * @return string + */ + protected function _dsn() + { + // baseline of DSN parts + $dsn = $this->_config; + + if (isset($dsn['host'])) { + $tns = 'dbname=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' . + '(HOST=' . $dsn['host'] . ')'; + + if (isset($dsn['port'])) { + $tns .= '(PORT=' . $dsn['port'] . ')'; + } else { + $tns .= '(PORT=1521)'; + } + + $tns .= '))(CONNECT_DATA=(SID=' . $dsn['dbname'] . ')))'; + } else { + $tns = 'dbname=' . $dsn['dbname']; + } + + if (isset($dsn['charset'])) { + $tns .= ';charset=' . $dsn['charset']; + } + + return $this->_pdoType . ':' . $tns; + } + + /** + * Quote a raw string. + * Most PDO drivers have an implementation for the quote() method, + * but the Oracle OCI driver must use the same implementation as the + * Zend_Db_Adapter_Abstract class. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (is_int($value) || is_float($value)) { + return $value; + } + $value = str_replace("'", "''", $value); + return "'" . addcslashes($value, "\000\n\r\\\032") . "'"; + } + + /** + * Quote a table identifier and alias. + * + * @param string|array|Expr $ident The identifier or expression. + * @param string $alias An alias for the table. + * @return string The quoted identifier and alias. + */ + public function quoteTableAs($ident, $alias = null, $auto = false) + { + // Oracle doesn't allow the 'AS' keyword between the table identifier/expression and alias. + return $this->_quoteIdentifierAs($ident, $alias, $auto, ' '); + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $data = $this->fetchCol('SELECT table_name FROM all_tables'); + return $data; + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @todo Discover integer unsigned property. + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + $version = $this->getServerVersion(); + if (($version === null) || version_compare($version, '9.0.0', '>=')) { + $sql = "SELECT TC.TABLE_NAME, TC.OWNER, TC.COLUMN_NAME, TC.DATA_TYPE, + TC.DATA_DEFAULT, TC.NULLABLE, TC.COLUMN_ID, TC.DATA_LENGTH, + TC.DATA_SCALE, TC.DATA_PRECISION, C.CONSTRAINT_TYPE, CC.POSITION + FROM ALL_TAB_COLUMNS TC + LEFT JOIN (ALL_CONS_COLUMNS CC JOIN ALL_CONSTRAINTS C + ON (CC.CONSTRAINT_NAME = C.CONSTRAINT_NAME AND CC.TABLE_NAME = C.TABLE_NAME + AND CC.OWNER = C.OWNER AND C.CONSTRAINT_TYPE = 'P')) + ON TC.TABLE_NAME = CC.TABLE_NAME AND TC.COLUMN_NAME = CC.COLUMN_NAME + WHERE UPPER(TC.TABLE_NAME) = UPPER(:TBNAME)"; + $bind[':TBNAME'] = $tableName; + if ($schemaName) { + $sql .= ' AND UPPER(TC.OWNER) = UPPER(:SCNAME)'; + $bind[':SCNAME'] = $schemaName; + } + $sql .= ' ORDER BY TC.COLUMN_ID'; + } else { + $subSql="SELECT AC.OWNER, AC.TABLE_NAME, ACC.COLUMN_NAME, AC.CONSTRAINT_TYPE, ACC.POSITION + from ALL_CONSTRAINTS AC, ALL_CONS_COLUMNS ACC + WHERE ACC.CONSTRAINT_NAME = AC.CONSTRAINT_NAME + AND ACC.TABLE_NAME = AC.TABLE_NAME + AND ACC.OWNER = AC.OWNER + AND AC.CONSTRAINT_TYPE = 'P' + AND UPPER(AC.TABLE_NAME) = UPPER(:TBNAME)"; + $bind[':TBNAME'] = $tableName; + if ($schemaName) { + $subSql .= ' AND UPPER(ACC.OWNER) = UPPER(:SCNAME)'; + $bind[':SCNAME'] = $schemaName; + } + $sql="SELECT TC.TABLE_NAME, TC.OWNER, TC.COLUMN_NAME, TC.DATA_TYPE, + TC.DATA_DEFAULT, TC.NULLABLE, TC.COLUMN_ID, TC.DATA_LENGTH, + TC.DATA_SCALE, TC.DATA_PRECISION, CC.CONSTRAINT_TYPE, CC.POSITION + FROM ALL_TAB_COLUMNS TC, ($subSql) CC + WHERE UPPER(TC.TABLE_NAME) = UPPER(:TBNAME) + AND TC.OWNER = CC.OWNER(+) AND TC.TABLE_NAME = CC.TABLE_NAME(+) + AND TC.COLUMN_NAME = CC.COLUMN_NAME(+)"; + if ($schemaName) { + $sql .= ' AND UPPER(TC.OWNER) = UPPER(:SCNAME)'; + } + $sql .= ' ORDER BY TC.COLUMN_ID'; + } + + $stmt = $this->query($sql, $bind); + + /** + * Use FETCH_NUM so we are not dependent on the CASE attribute of the PDO connection + */ + $result = $stmt->fetchAll(Db::FETCH_NUM); + + $table_name = 0; + $owner = 1; + $column_name = 2; + $data_type = 3; + $data_default = 4; + $nullable = 5; + $column_id = 6; + $data_length = 7; + $data_scale = 8; + $data_precision = 9; + $constraint_type = 10; + $position = 11; + + $desc = array(); + foreach ($result as $key => $row) { + list ($primary, $primaryPosition, $identity) = array(false, null, false); + if ($row[$constraint_type] == 'P') { + $primary = true; + $primaryPosition = $row[$position]; + /** + * Oracle does not support auto-increment keys. + */ + $identity = false; + } + $desc[$this->foldCase($row[$column_name])] = array( + 'SCHEMA_NAME' => $this->foldCase($row[$owner]), + 'TABLE_NAME' => $this->foldCase($row[$table_name]), + 'COLUMN_NAME' => $this->foldCase($row[$column_name]), + 'COLUMN_POSITION' => $row[$column_id], + 'DATA_TYPE' => $row[$data_type], + 'DEFAULT' => $row[$data_default], + 'NULLABLE' => (bool) ($row[$nullable] == 'Y'), + 'LENGTH' => $row[$data_length], + 'SCALE' => $row[$data_scale], + 'PRECISION' => $row[$data_precision], + 'UNSIGNED' => null, // @todo + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + return $desc; + } + + /** + * Return the most recent value from the specified sequence in the database. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return integer + */ + public function lastSequenceId($sequenceName) + { + $this->_connect(); + $value = $this->fetchOne('SELECT '.$this->quoteIdentifier($sequenceName, true).'.CURRVAL FROM dual'); + return $value; + } + + /** + * Generate a new value from the specified sequence in the database, and return it. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return integer + */ + public function nextSequenceId($sequenceName) + { + $this->_connect(); + $value = $this->fetchOne('SELECT '.$this->quoteIdentifier($sequenceName, true).'.NEXTVAL FROM dual'); + return $value; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * Oracle does not support IDENTITY columns, so if the sequence is not + * specified, this method returns null. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + * @throws AdapterExceptionOracle + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + if ($tableName !== null) { + $sequenceName = $tableName; + if ($primaryKey) { + $sequenceName .= $this->foldCase("_$primaryKey"); + } + $sequenceName .= $this->foldCase('_seq'); + return $this->lastSequenceId($sequenceName); + } + // No support for IDENTITY columns; return null + return null; + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset + * @throws AdapterException + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + + /** + * Oracle does not implement the LIMIT clause as some RDBMS do. + * We have to simulate it with subqueries and ROWNUM. + * Unfortunately because we use the column wildcard "*", + * this puts an extra column into the query result set. + */ + $limit_sql = "SELECT z2.* + FROM ( + SELECT z1.*, ROWNUM AS \"zend_db_rownum\" + FROM ( + " . $sql . " + ) z1 + ) z2 + WHERE z2.\"zend_db_rownum\" BETWEEN " . ($offset+1) . " AND " . ($offset+$count); + return $limit_sql; + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/PdoAdapter.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/PdoAdapter.php new file mode 100644 index 0000000..8d84e5d --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/PdoAdapter.php @@ -0,0 +1,367 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo; + +use gipfl\ZfDb\Adapter\Adapter; +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Profiler; +use gipfl\ZfDb\Select; +use gipfl\ZfDb\Statement\PdoStatement; +use gipfl\ZfDb\Statement\Exception\StatementException; +use PDO; +use PDOException; + +/** + * Class for connecting to SQL databases and performing common operations using PDO. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +abstract class PdoAdapter extends Adapter +{ + /** + * Default class name for a DB statement. + * + * @var string + */ + protected $_defaultStmtClass = PdoStatement::class; + + /** + * Creates a PDO DSN for the adapter from $this->_config settings. + * + * @return string + */ + protected function _dsn() + { + // baseline of DSN parts + $dsn = $this->_config; + + // don't pass the username, password, charset, persistent and driver_options in the DSN + unset($dsn['username']); + unset($dsn['password']); + unset($dsn['options']); + unset($dsn['charset']); + unset($dsn['persistent']); + unset($dsn['driver_options']); + + // use all remaining parts in the DSN + foreach ($dsn as $key => $val) { + $dsn[$key] = "$key=$val"; + } + + return $this->_pdoType . ':' . implode(';', $dsn); + } + + /** + * Creates a PDO object and connects to the database. + * + * @return void + * @throws AdapterException + */ + protected function _connect() + { + // if we already have a PDO object, no need to re-connect. + if ($this->_connection) { + return; + } + + // get the dsn first, because some adapters alter the $_pdoType + $dsn = $this->_dsn(); + + // check for PDO extension + if (!extension_loaded('pdo')) { + throw new AdapterException( + 'The PDO extension is required for this adapter but the extension is not loaded' + ); + } + + // check the PDO driver is available + if (!in_array($this->_pdoType, PDO::getAvailableDrivers())) { + throw new AdapterException('The ' . $this->_pdoType . ' driver is not currently installed'); + } + + // create PDO connection + $q = $this->_profiler->queryStart('connect', Profiler::CONNECT); + + // add the persistence flag if we find it in our config array + if (isset($this->_config['persistent']) && ($this->_config['persistent'] == true)) { + $this->_config['driver_options'][PDO::ATTR_PERSISTENT] = true; + } + + try { + $this->_connection = new PDO( + $dsn, + $this->_config['username'], + $this->_config['password'], + $this->_config['driver_options'] + ); + + $this->_profiler->queryEnd($q); + + // set the PDO connection to perform case-folding on array keys, or not + $this->_connection->setAttribute(PDO::ATTR_CASE, $this->_caseFolding); + + // always use exceptions. + $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $e) { + $message = $e->getMessage(); + if ($e->getPrevious() !== null && preg_match('~^SQLSTATE\[HY000\] \[\d{1,4}\]\s$~', $message)) { + // See https://bugs.php.net/bug.php?id=76604 + $message .= $e->getPrevious()->getMessage(); + } + + throw new AdapterException($message, $e->getCode(), $e); + } + } + + /** + * Test if a connection is active + * + * @return boolean + */ + public function isConnected() + { + return ((bool) ($this->_connection instanceof PDO)); + } + + /** + * Force the connection to close. + * + * @return void + */ + public function closeConnection() + { + $this->_connection = null; + } + + /** + * Prepares an SQL statement. + * + * @param string $sql The SQL statement with placeholders. + * @param array $bind An array of data to bind to the placeholders. + * @return PdoStatement + */ + public function prepare($sql) + { + $this->_connect(); + $stmtClass = $this->_defaultStmtClass; + $stmt = new $stmtClass($this, $sql); + $stmt->setFetchMode($this->_fetchMode); + return $stmt; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * On RDBMS brands that don't support sequences, $tableName and $primaryKey + * are ignored. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + $this->_connect(); + return $this->_connection->lastInsertId(); + } + + /** + * Special handling for PDO query(). + * All bind parameter names must begin with ':' + * + * @param string|Select $sql The SQL statement with placeholders. + * @param array $bind An array of data to bind to the placeholders. + * @return Statement + * @throws AdapterException To re-throw PDOException. + */ + public function query($sql, $bind = array()) + { + if (empty($bind) && $sql instanceof Select) { + $bind = $sql->getBind(); + } + + if (is_array($bind)) { + foreach ($bind as $name => $value) { + if (!is_int($name) && !preg_match('/^:/', $name)) { + $newName = ":$name"; + unset($bind[$name]); + $bind[$newName] = $value; + } + } + } + + try { + return parent::query($sql, $bind); + } catch (PDOException $e) { + /** + * @see StatementException + */ + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Executes an SQL statement and return the number of affected rows + * + * @param mixed $sql The SQL statement with placeholders. + * May be a string or Zend_Db_Select. + * @return integer Number of rows that were modified + * or deleted by the SQL statement + */ + public function exec($sql) + { + if ($sql instanceof Select) { + $sql = $sql->assemble(); + } + + try { + $affected = $this->getConnection()->exec($sql); + + if ($affected === false) { + $errorInfo = $this->getConnection()->errorInfo(); + throw new AdapterException($errorInfo[2]); + } + + return $affected; + } catch (PDOException $e) { + $code = $e->getCode(); + $code = ctype_digit($code) ? (int) $code : 0; + throw new AdapterException($e->getMessage(), $code, $e); + } + } + + /** + * Quote a raw string. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (is_int($value) || is_float($value)) { + return $value; + } + $this->_connect(); + return $this->_connection->quote($value); + } + + /** + * Begin a transaction. + */ + protected function _beginTransaction() + { + $this->_connect(); + $this->_connection->beginTransaction(); + } + + /** + * Commit a transaction. + */ + protected function _commit() + { + $this->_connect(); + $this->_connection->commit(); + } + + /** + * Roll-back a transaction. + */ + protected function _rollBack() + { + $this->_connect(); + $this->_connection->rollBack(); + } + + /** + * Set the PDO fetch mode. + * + * @todo Support FETCH_CLASS and FETCH_INTO. + * + * @param int $mode A PDO fetch mode. + * @return void + * @throws AdapterException + */ + public function setFetchMode($mode) + { + //check for PDO extension + if (!extension_loaded('pdo')) { + throw new AdapterException( + 'The PDO extension is required for this adapter but the extension is not loaded' + ); + } + switch ($mode) { + case PDO::FETCH_LAZY: + case PDO::FETCH_ASSOC: + case PDO::FETCH_NUM: + case PDO::FETCH_BOTH: + case PDO::FETCH_NAMED: + case PDO::FETCH_OBJ: + $this->_fetchMode = $mode; + break; + default: + throw new AdapterException("Invalid fetch mode '$mode' specified"); + } + } + + /** + * Check if the adapter supports real SQL parameters. + * + * @param string $type 'positional' or 'named' + * @return bool + */ + public function supportsParameters($type) + { + switch ($type) { + case 'positional': + case 'named': + default: + return true; + } + } + + /** + * Retrieve server version in PHP style + * + * @return string + */ + public function getServerVersion() + { + $this->_connect(); + try { + $version = $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } catch (PDOException $e) { + // In case of the driver doesn't support getting attributes + return null; + } + $matches = null; + if (preg_match('/((?:[0-9]{1,2}\.){1,3}[0-9]{1,2})/', $version, $matches)) { + return $matches[1]; + } else { + return null; + } + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Pgsql.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Pgsql.php new file mode 100644 index 0000000..fc08d38 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Pgsql.php @@ -0,0 +1,318 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Db; + +/** + * Class for connecting to PostgreSQL databases and performing common operations. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Pgsql extends PdoAdapter +{ + + /** + * PDO type. + * + * @var string + */ + protected $_pdoType = 'pgsql'; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INTEGER' => Db::INT_TYPE, + 'SERIAL' => Db::INT_TYPE, + 'SMALLINT' => Db::INT_TYPE, + 'BIGINT' => Db::BIGINT_TYPE, + 'BIGSERIAL' => Db::BIGINT_TYPE, + 'DECIMAL' => Db::FLOAT_TYPE, + 'DOUBLE PRECISION' => Db::FLOAT_TYPE, + 'NUMERIC' => Db::FLOAT_TYPE, + 'REAL' => Db::FLOAT_TYPE + ); + + /** + * Creates a PDO object and connects to the database. + * + * @return void + * @throws AdapterException + */ + protected function _connect() + { + if ($this->_connection) { + return; + } + + parent::_connect(); + + if (!empty($this->_config['charset'])) { + $sql = "SET NAMES '" . $this->_config['charset'] . "'"; + $this->_connection->exec($sql); + } + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + // @todo use a better query with joins instead of subqueries + $sql = "SELECT c.relname AS table_name " + . "FROM pg_class c, pg_user u " + . "WHERE c.relowner = u.usesysid AND c.relkind = 'r' " + . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) " + . "AND c.relname !~ '^(pg_|sql_)' " + . "UNION " + . "SELECT c.relname AS table_name " + . "FROM pg_class c " + . "WHERE c.relkind = 'r' " + . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) " + . "AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) " + . "AND c.relname !~ '^pg_'"; + + return $this->fetchCol($sql); + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @todo Discover integer unsigned property. + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + $sql = "SELECT + a.attnum, + n.nspname, + c.relname, + a.attname AS colname, + t.typname AS type, + a.atttypmod, + FORMAT_TYPE(a.atttypid, a.atttypmod) AS complete_type, + d.adsrc AS default_value, + a.attnotnull AS notnull, + a.attlen AS length, + co.contype, + ARRAY_TO_STRING(co.conkey, ',') AS conkey + FROM pg_attribute AS a + JOIN pg_class AS c ON a.attrelid = c.oid + JOIN pg_namespace AS n ON c.relnamespace = n.oid + JOIN pg_type AS t ON a.atttypid = t.oid + LEFT OUTER JOIN pg_constraint AS co ON (co.conrelid = c.oid + AND a.attnum = ANY(co.conkey) AND co.contype = 'p') + LEFT OUTER JOIN pg_attrdef AS d ON d.adrelid = c.oid AND d.adnum = a.attnum + WHERE a.attnum > 0 AND c.relname = ".$this->quote($tableName); + if ($schemaName) { + $sql .= " AND n.nspname = ".$this->quote($schemaName); + } + $sql .= ' ORDER BY a.attnum'; + + $stmt = $this->query($sql); + + // Use FETCH_NUM so we are not dependent on the CASE attribute of the PDO connection + $result = $stmt->fetchAll(Db::FETCH_NUM); + + $attnum = 0; + $nspname = 1; + $relname = 2; + $colname = 3; + $type = 4; + $atttypemod = 5; + $complete_type = 6; + $default_value = 7; + $notnull = 8; + $length = 9; + $contype = 10; + $conkey = 11; + + $desc = array(); + foreach ($result as $key => $row) { + $defaultValue = $row[$default_value]; + if ($row[$type] == 'varchar' || $row[$type] == 'bpchar') { + if (preg_match('/character(?: varying)?(?:\((\d+)\))?/', $row[$complete_type], $matches)) { + if (isset($matches[1])) { + $row[$length] = $matches[1]; + } else { + $row[$length] = null; // unlimited + } + } + if (preg_match("/^'(.*?)'::(?:character varying|bpchar)$/", $defaultValue, $matches)) { + $defaultValue = $matches[1]; + } + } + list($primary, $primaryPosition, $identity) = array(false, null, false); + if ($row[$contype] == 'p') { + $primary = true; + $primaryPosition = array_search($row[$attnum], explode(',', $row[$conkey])) + 1; + $identity = (bool) (preg_match('/^nextval/', $row[$default_value])); + } + $desc[$this->foldCase($row[$colname])] = array( + 'SCHEMA_NAME' => $this->foldCase($row[$nspname]), + 'TABLE_NAME' => $this->foldCase($row[$relname]), + 'COLUMN_NAME' => $this->foldCase($row[$colname]), + 'COLUMN_POSITION' => $row[$attnum], + 'DATA_TYPE' => $row[$type], + 'DEFAULT' => $defaultValue, + 'NULLABLE' => (bool) ($row[$notnull] != 't'), + 'LENGTH' => $row[$length], + 'SCALE' => null, // @todo + 'PRECISION' => null, // @todo + 'UNSIGNED' => null, // @todo + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + return $desc; + } + + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + + $sql .= " LIMIT $count"; + if ($offset > 0) { + $sql .= " OFFSET $offset"; + } + + return $sql; + } + + /** + * Return the most recent value from the specified sequence in the database. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function lastSequenceId($sequenceName) + { + $this->_connect(); + $sequenceName = str_replace($this->getQuoteIdentifierSymbol(), '', (string) $sequenceName); + $value = $this->fetchOne("SELECT CURRVAL(" + . $this->quote($this->quoteIdentifier($sequenceName, true)) + . ")"); + return $value; + } + + /** + * Generate a new value from the specified sequence in the database, and return it. + * This is supported only on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2). Other RDBMS brands return null. + * + * @param string $sequenceName + * @return string + */ + public function nextSequenceId($sequenceName) + { + $this->_connect(); + $sequenceName = str_replace($this->getQuoteIdentifierSymbol(), '', (string) $sequenceName); + $value = $this->fetchOne("SELECT NEXTVAL(" + . $this->quote($this->quoteIdentifier($sequenceName, true)) + . ")"); + return $value; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + if ($tableName !== null) { + $sequenceName = $tableName; + if ($primaryKey) { + $sequenceName .= "_$primaryKey"; + } + $sequenceName .= '_seq'; + return $this->lastSequenceId($sequenceName); + } + return $this->_connection->lastInsertId($tableName); + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Pdo/Sqlite.php b/vendor/gipfl/zfdb/src/Adapter/Pdo/Sqlite.php new file mode 100644 index 0000000..5a12912 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Pdo/Sqlite.php @@ -0,0 +1,293 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter\Pdo; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Db; + +/** + * Class for connecting to SQLite2 and SQLite3 databases and performing common operations. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Sqlite extends PdoAdapter +{ + /** + * PDO type + * + * @var string + */ + protected $_pdoType = 'sqlite'; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INTEGER' => Db::BIGINT_TYPE, + 'REAL' => Db::FLOAT_TYPE + ); + + /** + * Constructor. + * + * $config is an array of key/value pairs containing configuration + * options. Note that the SQLite options are different than most of + * the other PDO adapters in that no username or password are needed. + * Also, an extra config key "sqlite2" specifies compatibility mode. + * + * dbname => (string) The name of the database to user (required, + * use :memory: for memory-based database) + * + * sqlite2 => (boolean) PDO_SQLITE defaults to SQLite 3. For compatibility + * with an older SQLite 2 database, set this to TRUE. + * + * @param array $config An array of configuration keys. + */ + public function __construct(array $config = array()) + { + if (isset($config['sqlite2']) && $config['sqlite2']) { + $this->_pdoType = 'sqlite2'; + } + + // SQLite uses no username/password. Stub to satisfy parent::_connect() + $this->_config['username'] = null; + $this->_config['password'] = null; + + return parent::__construct($config); + } + + /** + * Check for config options that are mandatory. + * Throw exceptions if any are missing. + * + * @param array $config + * @throws AdapterException + */ + protected function _checkRequiredOptions(array $config) + { + // we need at least a dbname + if (! array_key_exists('dbname', $config)) { + throw new AdapterException( + "Configuration array must have a key for 'dbname' that names the database instance" + ); + } + } + + /** + * DSN builder + */ + protected function _dsn() + { + return $this->_pdoType .':'. $this->_config['dbname']; + } + + /** + * Special configuration for SQLite behavior: make sure that result sets + * contain keys like 'column' instead of 'table.column'. + * + * @throws AdapterException + */ + protected function _connect() + { + /** + * if we already have a PDO object, no need to re-connect. + */ + if ($this->_connection) { + return; + } + + parent::_connect(); + + $retval = $this->_connection->exec('PRAGMA full_column_names=0'); + if ($retval === false) { + $error = $this->_connection->errorInfo(); + throw new AdapterException($error[2]); + } + + $retval = $this->_connection->exec('PRAGMA short_column_names=1'); + if ($retval === false) { + $error = $this->_connection->errorInfo(); + throw new AdapterException($error[2]); + } + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $sql = "SELECT name FROM sqlite_master WHERE type='table' " + . "UNION ALL SELECT name FROM sqlite_temp_master " + . "WHERE type='table' ORDER BY name"; + + return $this->fetchCol($sql); + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of database or schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + $sql = 'PRAGMA '; + + if ($schemaName) { + $sql .= $this->quoteIdentifier($schemaName) . '.'; + } + + $sql .= 'table_info('.$this->quoteIdentifier($tableName).')'; + + $stmt = $this->query($sql); + + /** + * Use FETCH_NUM so we are not dependent on the CASE attribute of the PDO connection + */ + $result = $stmt->fetchAll(Db::FETCH_NUM); + + $cid = 0; + $name = 1; + $type = 2; + $notnull = 3; + $dflt_value = 4; + $pk = 5; + + $desc = array(); + + $p = 1; + foreach ($result as $key => $row) { + list($length, $scale, $precision, $primary, $primaryPosition, $identity) = + array(null, null, null, false, null, false); + if (preg_match('/^((?:var)?char)\((\d+)\)/i', $row[$type], $matches)) { + $row[$type] = $matches[1]; + $length = $matches[2]; + } elseif (preg_match('/^decimal\((\d+),(\d+)\)/i', $row[$type], $matches)) { + $row[$type] = 'DECIMAL'; + $precision = $matches[1]; + $scale = $matches[2]; + } + if ((bool) $row[$pk]) { + $primary = true; + $primaryPosition = $p; + /** + * SQLite INTEGER primary key is always auto-increment. + */ + $identity = (bool) ($row[$type] == 'INTEGER'); + ++$p; + } + $desc[$this->foldCase($row[$name])] = array( + 'SCHEMA_NAME' => $this->foldCase($schemaName), + 'TABLE_NAME' => $this->foldCase($tableName), + 'COLUMN_NAME' => $this->foldCase($row[$name]), + 'COLUMN_POSITION' => $row[$cid]+1, + 'DATA_TYPE' => $row[$type], + 'DEFAULT' => $row[$dflt_value], + 'NULLABLE' => ! (bool) $row[$notnull], + 'LENGTH' => $length, + 'SCALE' => $scale, + 'PRECISION' => $precision, + 'UNSIGNED' => null, // Sqlite3 does not support unsigned data + 'PRIMARY' => $primary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity + ); + } + return $desc; + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @return string + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + + $sql .= " LIMIT $count"; + if ($offset > 0) { + $sql .= " OFFSET $offset"; + } + + return $sql; + } + + /** + * Quote a raw string. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (!is_int($value) && !is_float($value)) { + // Fix for null-byte injection + $value = addcslashes($value, "\000\032"); + } + return parent::_quote($value); + } +} diff --git a/vendor/gipfl/zfdb/src/Adapter/Sqlsrv.php b/vendor/gipfl/zfdb/src/Adapter/Sqlsrv.php new file mode 100644 index 0000000..c940a75 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Adapter/Sqlsrv.php @@ -0,0 +1,640 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Adapter; + +use gipfl\ZfDb\Adapter\Exception\AdapterException; +use gipfl\ZfDb\Adapter\Exception\AdapterExceptionSqlsrv; +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Expr; +use gipfl\ZfDb\Statement\SqlsrvStatement; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Sqlsrv extends Adapter +{ + /** + * User-provided configuration. + * + * Basic keys are: + * + * username => (string) Connect to the database as this username. + * password => (string) Password associated with the username. + * dbname => The name of the local SQL Server instance + * + * @var array + */ + protected $_config = array( + 'dbname' => null, + 'username' => null, + 'password' => null, + ); + + /** + * Last insert id from INSERT query + * + * @var int + */ + protected $_lastInsertId; + + /** + * Query used to fetch last insert id + * + * @var string + */ + protected $_lastInsertSQL = 'SELECT SCOPE_IDENTITY() as Current_Identity'; + + /** + * Keys are UPPERCASE SQL datatypes or the constants + * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. + * + * Values are: + * 0 = 32-bit integer + * 1 = 64-bit integer + * 2 = float or decimal + * + * @var array Associative array of datatypes to values 0, 1, or 2. + */ + protected $_numericDataTypes = array( + Db::INT_TYPE => Db::INT_TYPE, + Db::BIGINT_TYPE => Db::BIGINT_TYPE, + Db::FLOAT_TYPE => Db::FLOAT_TYPE, + 'INT' => Db::INT_TYPE, + 'SMALLINT' => Db::INT_TYPE, + 'TINYINT' => Db::INT_TYPE, + 'BIGINT' => Db::BIGINT_TYPE, + 'DECIMAL' => Db::FLOAT_TYPE, + 'FLOAT' => Db::FLOAT_TYPE, + 'MONEY' => Db::FLOAT_TYPE, + 'NUMERIC' => Db::FLOAT_TYPE, + 'REAL' => Db::FLOAT_TYPE, + 'SMALLMONEY' => Db::FLOAT_TYPE, + ); + + /** + * Default class name for a DB statement. + * + * @var string + */ + protected $_defaultStmtClass = SqlsrvStatement::class; + + /** + * Creates a connection resource. + * + * @return void + * @throws AdapterExceptionSqlsrv + */ + protected function _connect() + { + if (is_resource($this->_connection)) { + // connection already exists + return; + } + + if (!extension_loaded('sqlsrv')) { + throw new AdapterExceptionSqlsrv( + 'The Sqlsrv extension is required for this adapter but the extension is not loaded' + ); + } + + $serverName = $this->_config['host']; + if (isset($this->_config['port'])) { + $port = (integer) $this->_config['port']; + $serverName .= ', ' . $port; + } + + $connectionInfo = array( + 'Database' => $this->_config['dbname'], + ); + + if (isset($this->_config['username']) && isset($this->_config['password'])) { + $connectionInfo += array( + 'UID' => $this->_config['username'], + 'PWD' => $this->_config['password'], + ); + } + // else - windows authentication + + if (!empty($this->_config['driver_options'])) { + foreach ($this->_config['driver_options'] as $option => $value) { + // A value may be a constant. + if (is_string($value)) { + $constantName = strtoupper($value); + if (defined($constantName)) { + $connectionInfo[$option] = constant($constantName); + } else { + $connectionInfo[$option] = $value; + } + } + } + } + + $this->_connection = sqlsrv_connect($serverName, $connectionInfo); + + if (!$this->_connection) { + throw new AdapterExceptionSqlsrv(sqlsrv_errors()); + } + } + + /** + * Check for config options that are mandatory. + * Throw exceptions if any are missing. + * + * @param array $config + * @throws AdapterException + */ + protected function _checkRequiredOptions(array $config) + { + // we need at least a dbname + if (! array_key_exists('dbname', $config)) { + throw new AdapterException( + "Configuration array must have a key for 'dbname' that names the database instance" + ); + } + + if (! array_key_exists('password', $config) && array_key_exists('username', $config)) { + throw new AdapterException( + "Configuration array must have a key for 'password' for login credentials." + . " If Windows Authentication is desired, both keys 'username' and 'password'" + . " should be ommited from config." + ); + } + + if (array_key_exists('password', $config) && !array_key_exists('username', $config)) { + throw new AdapterException( + "Configuration array must have a key for 'username' for login credentials." + . " If Windows Authentication is desired, both keys 'username' and 'password'" + . " should be ommited from config." + ); + } + } + + /** + * Set the transaction isoltion level. + * + * @param integer|null $level A fetch mode from SQLSRV_TXN_*. + * @return true + * @throws AdapterExceptionSqlsrv + */ + public function setTransactionIsolationLevel($level = null) + { + $this->_connect(); + $sql = null; + + // Default transaction level in sql server + if ($level === null) { + $level = SQLSRV_TXN_READ_COMMITTED; + } + + switch ($level) { + case SQLSRV_TXN_READ_UNCOMMITTED: + $sql = "READ UNCOMMITTED"; + break; + case SQLSRV_TXN_READ_COMMITTED: + $sql = "READ COMMITTED"; + break; + case SQLSRV_TXN_REPEATABLE_READ: + $sql = "REPEATABLE READ"; + break; + case SQLSRV_TXN_SNAPSHOT: + $sql = "SNAPSHOT"; + break; + case SQLSRV_TXN_SERIALIZABLE: + $sql = "SERIALIZABLE"; + break; + default: + throw new AdapterExceptionSqlsrv("Invalid transaction isolation level mode '$level' specified"); + } + + if (!sqlsrv_query($this->_connection, "SET TRANSACTION ISOLATION LEVEL $sql;")) { + throw new AdapterExceptionSqlsrv("Transaction cannot be changed to '$level'"); + } + + return true; + } + + /** + * Test if a connection is active + * + * @return boolean + */ + public function isConnected() + { + return (is_resource($this->_connection) + && (get_resource_type($this->_connection) == 'SQL Server Connection') + ); + } + + /** + * Force the connection to close. + * + * @return void + */ + public function closeConnection() + { + if ($this->isConnected()) { + sqlsrv_close($this->_connection); + } + $this->_connection = null; + } + + /** + * Returns an SQL statement for preparation. + * + * @param string $sql The SQL statement with placeholders. + * @return SqlsrvStatement + */ + public function prepare($sql) + { + $this->_connect(); + $stmtClass = $this->_defaultStmtClass; + $stmt = new $stmtClass($this, $sql); + $stmt->setFetchMode($this->_fetchMode); + return $stmt; + } + + /** + * Quote a raw string. + * + * @param string $value Raw string + * @return string Quoted string + */ + protected function _quote($value) + { + if (is_int($value)) { + return $value; + } elseif (is_float($value)) { + return sprintf('%F', $value); + } + + $value = addcslashes($value, "\000\032"); + return "'" . str_replace("'", "''", $value) . "'"; + } + + /** + * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. + * + * As a convention, on RDBMS brands that support sequences + * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence + * from the arguments and returns the last id generated by that sequence. + * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method + * returns the last value generated for such a column, and the table name + * argument is disregarded. + * + * @param string $tableName OPTIONAL Name of table. + * @param string $primaryKey OPTIONAL Name of primary key column. + * @return string + */ + public function lastInsertId($tableName = null, $primaryKey = null) + { + if ($tableName) { + $tableName = $this->quote($tableName); + $sql = 'SELECT IDENT_CURRENT (' . $tableName . ') as Current_Identity'; + return (string) $this->fetchOne($sql); + } + + if ($this->_lastInsertId > 0) { + return (string) $this->_lastInsertId; + } + + $sql = $this->_lastInsertSQL; + return (string) $this->fetchOne($sql); + } + + /** + * Inserts a table row with specified data. + * + * @param mixed $table The table to insert data into. + * @param array $bind Column-value pairs. + * @return int The number of affected rows. + */ + public function insert($table, array $bind) + { + // extract and quote col names from the array keys + $cols = array(); + $vals = array(); + foreach ($bind as $col => $val) { + $cols[] = $this->quoteIdentifier($col, true); + if ($val instanceof Expr) { + $vals[] = $val->__toString(); + unset($bind[$col]); + } else { + $vals[] = '?'; + } + } + + // build the statement + $sql = "INSERT INTO " + . $this->quoteIdentifier($table, true) + . ' (' . implode(', ', $cols) . ') ' + . 'VALUES (' . implode(', ', $vals) . ')' + . ' ' . $this->_lastInsertSQL; + + // execute the statement and return the number of affected rows + $stmt = $this->query($sql, array_values($bind)); + $result = $stmt->rowCount(); + + $stmt->nextRowset(); + + $this->_lastInsertId = $stmt->fetchColumn(); + + return $result; + } + + /** + * Returns a list of the tables in the database. + * + * @return array + */ + public function listTables() + { + $this->_connect(); + $sql = "SELECT name FROM sysobjects WHERE type = 'U' ORDER BY name"; + return $this->fetchCol($sql); + } + + /** + * Returns the column descriptions for a table. + * + * The return value is an associative array keyed by the column name, + * as returned by the RDBMS. + * + * The value of each array element is an associative array + * with the following keys: + * + * SCHEMA_NAME => string; name of schema + * TABLE_NAME => string; + * COLUMN_NAME => string; column name + * COLUMN_POSITION => number; ordinal position of column in table + * DATA_TYPE => string; SQL datatype name of column + * DEFAULT => string; default expression of column, null if none + * NULLABLE => boolean; true if column can have nulls + * LENGTH => number; length of CHAR/VARCHAR + * SCALE => number; scale of NUMERIC/DECIMAL + * PRECISION => number; precision of NUMERIC/DECIMAL + * UNSIGNED => boolean; unsigned property of an integer type + * PRIMARY => boolean; true if column is part of the primary key + * PRIMARY_POSITION => integer; position of column in primary key + * IDENTITY => integer; true if column is auto-generated with unique values + * + * @todo Discover integer unsigned property. + * + * @param string $tableName + * @param string $schemaName OPTIONAL + * @return array + */ + public function describeTable($tableName, $schemaName = null) + { + /** + * Discover metadata information about this table. + */ + $sql = "exec sp_columns @table_name = " . $this->quoteIdentifier($tableName, true); + $stmt = $this->query($sql); + $result = $stmt->fetchAll(Db::FETCH_NUM); + + // ZF-7698 + $stmt->closeCursor(); + + if (count($result) == 0) { + return array(); + } + + $owner = 1; + $table_name = 2; + $column_name = 3; + $type_name = 5; + $precision = 6; + $length = 7; + $scale = 8; + $nullable = 10; + $column_def = 12; + $column_position = 16; + + /** + * Discover primary key column(s) for this table. + */ + $tableOwner = $result[0][$owner]; + $sql = "exec sp_pkeys @table_owner = " . $tableOwner + . ", @table_name = " . $this->quoteIdentifier($tableName, true); + $stmt = $this->query($sql); + + $primaryKeysResult = $stmt->fetchAll(Db::FETCH_NUM); + $primaryKeyColumn = array(); + + // Per http://msdn.microsoft.com/en-us/library/ms189813.aspx, + // results from sp_keys stored procedure are: + // 0=TABLE_QUALIFIER 1=TABLE_OWNER 2=TABLE_NAME 3=COLUMN_NAME 4=KEY_SEQ 5=PK_NAME + + $pkey_column_name = 3; + $pkey_key_seq = 4; + foreach ($primaryKeysResult as $pkeysRow) { + $primaryKeyColumn[$pkeysRow[$pkey_column_name]] = $pkeysRow[$pkey_key_seq]; + } + + $desc = array(); + $p = 1; + foreach ($result as $key => $row) { + $identity = false; + $words = explode(' ', $row[$type_name], 2); + if (isset($words[0])) { + $type = $words[0]; + if (isset($words[1])) { + $identity = (bool) preg_match('/identity/', $words[1]); + } + } + + $isPrimary = array_key_exists($row[$column_name], $primaryKeyColumn); + if ($isPrimary) { + $primaryPosition = $primaryKeyColumn[$row[$column_name]]; + } else { + $primaryPosition = null; + } + + $desc[$this->foldCase($row[$column_name])] = array( + 'SCHEMA_NAME' => null, // @todo + 'TABLE_NAME' => $this->foldCase($row[$table_name]), + 'COLUMN_NAME' => $this->foldCase($row[$column_name]), + 'COLUMN_POSITION' => (int) $row[$column_position], + 'DATA_TYPE' => $type, + 'DEFAULT' => $row[$column_def], + 'NULLABLE' => (bool) $row[$nullable], + 'LENGTH' => $row[$length], + 'SCALE' => $row[$scale], + 'PRECISION' => $row[$precision], + 'UNSIGNED' => null, // @todo + 'PRIMARY' => $isPrimary, + 'PRIMARY_POSITION' => $primaryPosition, + 'IDENTITY' => $identity, + ); + } + + return $desc; + } + + /** + * Leave autocommit mode and begin a transaction. + * + * @return void + * @throws AdapterExceptionSqlsrv + */ + protected function _beginTransaction() + { + if (!sqlsrv_begin_transaction($this->_connection)) { + throw new AdapterExceptionSqlsrv(sqlsrv_errors()); + } + } + + /** + * Commit a transaction and return to autocommit mode. + * + * @return void + * @throws AdapterExceptionSqlsrv + */ + protected function _commit() + { + if (!sqlsrv_commit($this->_connection)) { + throw new AdapterExceptionSqlsrv(sqlsrv_errors()); + } + } + + /** + * Roll back a transaction and return to autocommit mode. + * + * @return void + * @throws AdapterExceptionSqlsrv + */ + protected function _rollBack() + { + if (!sqlsrv_rollback($this->_connection)) { + throw new AdapterExceptionSqlsrv(sqlsrv_errors()); + } + } + + /** + * Set the fetch mode. + * + * @param integer $mode A fetch mode. + * @return void + * @throws AdapterExceptionSqlsrv + *@todo Support FETCH_CLASS and FETCH_INTO. + * + */ + public function setFetchMode($mode) + { + switch ($mode) { + case Db::FETCH_NUM: // seq array + case Db::FETCH_ASSOC: // assoc array + case Db::FETCH_BOTH: // seq+assoc array + case Db::FETCH_OBJ: // object + $this->_fetchMode = $mode; + break; + case Db::FETCH_BOUND: // bound to PHP variable + throw new AdapterExceptionSqlsrv('FETCH_BOUND is not supported yet'); + default: + throw new AdapterExceptionSqlsrv("Invalid fetch mode '$mode' specified"); + } + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $sql + * @param integer $count + * @param integer $offset OPTIONAL + * @return string + * @throws AdapterException + */ + public function limit($sql, $count, $offset = 0) + { + $count = intval($count); + if ($count <= 0) { + throw new AdapterException("LIMIT argument count=$count is not valid"); + } + + $offset = intval($offset); + if ($offset < 0) { + /** @see AdapterException */ + throw new AdapterException("LIMIT argument offset=$offset is not valid"); + } + + if ($offset == 0) { + $sql = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . $count . ' ', $sql); + } else { + $orderby = stristr($sql, 'ORDER BY'); + + if (!$orderby) { + $over = 'ORDER BY (SELECT 0)'; + } else { + $over = preg_replace('/\"[^,]*\".\"([^,]*)\"/i', '"inner_tbl"."$1"', $orderby); + } + + // Remove ORDER BY clause from $sql + $sql = preg_replace('/\s+ORDER BY(.*)/', '', $sql); + + // Add ORDER BY clause as an argument for ROW_NUMBER() + $sql = "SELECT ROW_NUMBER() OVER ($over) AS \"ZEND_DB_ROWNUM\", * FROM ($sql) AS inner_tbl"; + + $start = $offset + 1; + + if ($count == PHP_INT_MAX) { + $sql = "WITH outer_tbl AS ($sql) SELECT * FROM outer_tbl WHERE \"ZEND_DB_ROWNUM\" >= $start"; + } else { + $end = $offset + $count; + $sql = "WITH outer_tbl AS ($sql) SELECT * FROM outer_tbl" + . " WHERE \"ZEND_DB_ROWNUM\" BETWEEN $start AND $end"; + } + } + + return $sql; + } + + /** + * Check if the adapter supports real SQL parameters. + * + * @param string $type 'positional' or 'named' + * @return bool + */ + public function supportsParameters($type) + { + if ($type == 'positional') { + return true; + } + + // if its 'named' or anything else + return false; + } + + /** + * Retrieve server version in PHP style + * + * @return string + */ + public function getServerVersion() + { + $this->_connect(); + $serverInfo = sqlsrv_server_info($this->_connection); + + if ($serverInfo !== false) { + return $serverInfo['SQLServerVersion']; + } + + return null; + } +} diff --git a/vendor/gipfl/zfdb/src/Db.php b/vendor/gipfl/zfdb/src/Db.php new file mode 100644 index 0000000..529461b --- /dev/null +++ b/vendor/gipfl/zfdb/src/Db.php @@ -0,0 +1,248 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb; + +use gipfl\ZfDb\Adapter\Adapter; +use gipfl\ZfDb\Exception\DbException; + +/** + * Class for connecting to SQL databases and performing common operations. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Db +{ + /** + * Use the PROFILER constant in the config of a Zend_Db_Adapter. + */ + const PROFILER = 'profiler'; + + /** + * Use the CASE_FOLDING constant in the config of a Zend_Db_Adapter. + */ + const CASE_FOLDING = 'caseFolding'; + + /** + * Use the FETCH_MODE constant in the config of a Zend_Db_Adapter. + */ + const FETCH_MODE = 'fetchMode'; + + /** + * Use the AUTO_QUOTE_IDENTIFIERS constant in the config of a Zend_Db_Adapter. + */ + const AUTO_QUOTE_IDENTIFIERS = 'autoQuoteIdentifiers'; + + /** + * Use the ALLOW_SERIALIZATION constant in the config of a Zend_Db_Adapter. + */ + const ALLOW_SERIALIZATION = 'allowSerialization'; + + /** + * Use the AUTO_RECONNECT_ON_UNSERIALIZE constant in the config of a Zend_Db_Adapter. + */ + const AUTO_RECONNECT_ON_UNSERIALIZE = 'autoReconnectOnUnserialize'; + + /** + * Use the INT_TYPE, BIGINT_TYPE, and FLOAT_TYPE with the quote() method. + */ + const INT_TYPE = 0; + const BIGINT_TYPE = 1; + const FLOAT_TYPE = 2; + + /** + * PDO constant values discovered by this script result: + * + * $list = array( + * 'PARAM_BOOL', 'PARAM_NULL', 'PARAM_INT', 'PARAM_STR', 'PARAM_LOB', + * 'PARAM_STMT', 'PARAM_INPUT_OUTPUT', 'FETCH_LAZY', 'FETCH_ASSOC', + * 'FETCH_NUM', 'FETCH_BOTH', 'FETCH_OBJ', 'FETCH_BOUND', + * 'FETCH_COLUMN', 'FETCH_CLASS', 'FETCH_INTO', 'FETCH_FUNC', + * 'FETCH_GROUP', 'FETCH_UNIQUE', 'FETCH_CLASSTYPE', 'FETCH_SERIALIZE', + * 'FETCH_NAMED', 'ATTR_AUTOCOMMIT', 'ATTR_PREFETCH', 'ATTR_TIMEOUT', + * 'ATTR_ERRMODE', 'ATTR_SERVER_VERSION', 'ATTR_CLIENT_VERSION', + * 'ATTR_SERVER_INFO', 'ATTR_CONNECTION_STATUS', 'ATTR_CASE', + * 'ATTR_CURSOR_NAME', 'ATTR_CURSOR', 'ATTR_ORACLE_NULLS', + * 'ATTR_PERSISTENT', 'ATTR_STATEMENT_CLASS', 'ATTR_FETCH_TABLE_NAMES', + * 'ATTR_FETCH_CATALOG_NAMES', 'ATTR_DRIVER_NAME', + * 'ATTR_STRINGIFY_FETCHES', 'ATTR_MAX_COLUMN_LEN', 'ERRMODE_SILENT', + * 'ERRMODE_WARNING', 'ERRMODE_EXCEPTION', 'CASE_NATURAL', + * 'CASE_LOWER', 'CASE_UPPER', 'NULL_NATURAL', 'NULL_EMPTY_STRING', + * 'NULL_TO_STRING', 'ERR_NONE', 'FETCH_ORI_NEXT', + * 'FETCH_ORI_PRIOR', 'FETCH_ORI_FIRST', 'FETCH_ORI_LAST', + * 'FETCH_ORI_ABS', 'FETCH_ORI_REL', 'CURSOR_FWDONLY', 'CURSOR_SCROLL', + * 'ERR_CANT_MAP', 'ERR_SYNTAX', 'ERR_CONSTRAINT', 'ERR_NOT_FOUND', + * 'ERR_ALREADY_EXISTS', 'ERR_NOT_IMPLEMENTED', 'ERR_MISMATCH', + * 'ERR_TRUNCATED', 'ERR_DISCONNECTED', 'ERR_NO_PERM', + * ); + * + * $const = array(); + * foreach ($list as $name) { + * $const[$name] = constant("PDO::$name"); + * } + * var_export($const); + */ + const ATTR_AUTOCOMMIT = 0; + const ATTR_CASE = 8; + const ATTR_CLIENT_VERSION = 5; + const ATTR_CONNECTION_STATUS = 7; + const ATTR_CURSOR = 10; + const ATTR_CURSOR_NAME = 9; + const ATTR_DRIVER_NAME = 16; + const ATTR_ERRMODE = 3; + const ATTR_FETCH_CATALOG_NAMES = 15; + const ATTR_FETCH_TABLE_NAMES = 14; + const ATTR_MAX_COLUMN_LEN = 18; + const ATTR_ORACLE_NULLS = 11; + const ATTR_PERSISTENT = 12; + const ATTR_PREFETCH = 1; + const ATTR_SERVER_INFO = 6; + const ATTR_SERVER_VERSION = 4; + const ATTR_STATEMENT_CLASS = 13; + const ATTR_STRINGIFY_FETCHES = 17; + const ATTR_TIMEOUT = 2; + const CASE_LOWER = 2; + const CASE_NATURAL = 0; + const CASE_UPPER = 1; + const CURSOR_FWDONLY = 0; + const CURSOR_SCROLL = 1; + const ERR_ALREADY_EXISTS = null; + const ERR_CANT_MAP = null; + const ERR_CONSTRAINT = null; + const ERR_DISCONNECTED = null; + const ERR_MISMATCH = null; + const ERR_NO_PERM = null; + const ERR_NONE = '00000'; + const ERR_NOT_FOUND = null; + const ERR_NOT_IMPLEMENTED = null; + const ERR_SYNTAX = null; + const ERR_TRUNCATED = null; + const ERRMODE_EXCEPTION = 2; + const ERRMODE_SILENT = 0; + const ERRMODE_WARNING = 1; + const FETCH_ASSOC = 2; + const FETCH_BOTH = 4; + const FETCH_BOUND = 6; + const FETCH_CLASS = 8; + const FETCH_CLASSTYPE = 262144; + const FETCH_COLUMN = 7; + const FETCH_FUNC = 10; + const FETCH_GROUP = 65536; + const FETCH_INTO = 9; + const FETCH_LAZY = 1; + const FETCH_NAMED = 11; + const FETCH_NUM = 3; + const FETCH_OBJ = 5; + const FETCH_ORI_ABS = 4; + const FETCH_ORI_FIRST = 2; + const FETCH_ORI_LAST = 3; + const FETCH_ORI_NEXT = 0; + const FETCH_ORI_PRIOR = 1; + const FETCH_ORI_REL = 5; + const FETCH_SERIALIZE = 524288; + const FETCH_UNIQUE = 196608; + const NULL_EMPTY_STRING = 1; + const NULL_NATURAL = 0; + const NULL_TO_STRING = null; + const PARAM_BOOL = 5; + const PARAM_INPUT_OUTPUT = -2147483648; + const PARAM_INT = 1; + const PARAM_LOB = 3; + const PARAM_NULL = 0; + const PARAM_STMT = 4; + const PARAM_STR = 2; + + /** + * Factory for Zend_Db_Adapter_Abstract classes. + * + * First argument may be a string containing the base of the adapter class + * name, e.g. 'Mysqli' corresponds to class Zend_Db_Adapter_Mysqli. This + * name is currently case-insensitive, but is not ideal to rely on this behavior. + * If your class is named 'My_Company_Pdo_Mysql', where 'My_Company' is the namespace + * and 'Pdo_Mysql' is the adapter name, it is best to use the name exactly as it + * is defined in the class. This will ensure proper use of the factory API. + * + * First argument may alternatively be an object of type Zend_Config. + * The adapter class base name is read from the 'adapter' property. + * The adapter config parameters are read from the 'params' property. + * + * Second argument is optional and may be an associative array of key-value + * pairs. This is used as the argument to the adapter constructor. + * + * If the first argument is of type Zend_Config, it is assumed to contain + * all parameters, and the second argument is ignored. + * + * @param mixed $adapter String name of base adapter class, or Zend_Config object. + * @param mixed $config OPTIONAL; an array or Zend_Config object with adapter parameters. + * @return Adapter + * @throws DbException + */ + public static function factory($adapter, $config = array()) + { + if ($config instanceof Zend_Config) { + $config = $config->toArray(); + } + + /* + * Convert Zend_Config argument to plain string + * adapter name and separate config object. + */ + if ($adapter instanceof Zend_Config) { + if (isset($adapter->params)) { + $config = $adapter->params->toArray(); + } + if (isset($adapter->adapter)) { + $adapter = (string) $adapter->adapter; + } else { + $adapter = null; + } + } + + /* + * Verify that adapter parameters are in an array. + */ + if (!is_array($config)) { + throw new DbException('Adapter parameters must be in an array or a Zend_Config object'); + } + + /* + * Verify that an adapter name has been specified. + */ + if (!is_string($adapter) || empty($adapter)) { + throw new DbException('Adapter name must be specified in a string'); + } + + /* + * Form full adapter class name + */ + $adapterClass = '\\gipfl\\ZfDb\\Adapter\\' + . str_replace(' ', '\\', ucwords(str_replace('_', ' ', strtolower($adapter)))); + $dbAdapter = new $adapterClass($config); + + /* + * Verify that the object created is a descendent of the abstract adapter type. + */ + if (! $dbAdapter instanceof Adapter) { + throw new DbException("Adapter class '$adapterClass' does not extend gipfl\ZfDb\Abstract"); + } + + return $dbAdapter; + } +} diff --git a/vendor/gipfl/zfdb/src/Exception/DbException.php b/vendor/gipfl/zfdb/src/Exception/DbException.php new file mode 100644 index 0000000..ab8c740 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Exception/DbException.php @@ -0,0 +1,29 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Exception; + +use Exception; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class DbException extends Exception +{ +} diff --git a/vendor/gipfl/zfdb/src/Exception/SelectException.php b/vendor/gipfl/zfdb/src/Exception/SelectException.php new file mode 100644 index 0000000..8764741 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Exception/SelectException.php @@ -0,0 +1,27 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Exception; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class SelectException extends DbException +{ +} diff --git a/vendor/gipfl/zfdb/src/Expr.php b/vendor/gipfl/zfdb/src/Expr.php new file mode 100644 index 0000000..2184c5a --- /dev/null +++ b/vendor/gipfl/zfdb/src/Expr.php @@ -0,0 +1,70 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb; + +/** + * Class for SQL SELECT fragments. + * + * This class simply holds a string, so that fragments of SQL statements can be + * distinguished from identifiers and values that should be implicitly quoted + * when interpolated into SQL statements. + * + * For example, when specifying a primary key value when inserting into a new + * row, some RDBMS brands may require you to use an expression to generate the + * new value of a sequence. If this expression is treated as an identifier, + * it will be quoted and the expression will not be evaluated. Another example + * is that you can use Expr in the Select::order() method to + * order by an expression instead of simply a column name. + * + * The way this works is that in each context in which a column name can be + * specified to methods of Zend_Db classes, if the value is an instance of + * Expr instead of a plain string, then the expression is not quoted. + * If it is a plain string, it is assumed to be a plain column name. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Expr +{ + /** + * Storage for the SQL expression. + * + * @var string + */ + protected $_expression; + + /** + * Instantiate an expression, which is just a string stored as + * an instance member variable. + * + * @param string $expression The string containing a SQL expression. + */ + public function __construct($expression) + { + $this->_expression = (string) $expression; + } + + /** + * @return string The string of the SQL expression stored in this object. + */ + public function __toString() + { + return $this->_expression; + } +} diff --git a/vendor/gipfl/zfdb/src/Profiler.php b/vendor/gipfl/zfdb/src/Profiler.php new file mode 100644 index 0000000..026fa0e --- /dev/null +++ b/vendor/gipfl/zfdb/src/Profiler.php @@ -0,0 +1,463 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb; + +use gipfl\ZfDb\Profiler\ProfilerException; +use gipfl\ZfDb\Profiler\ProfilerQuery; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Profiler +{ + /** + * A connection operation or selecting a database. + */ + const CONNECT = 1; + + /** + * Any general database query that does not fit into the other constants. + */ + const QUERY = 2; + + /** + * Adding new data to the database, such as SQL's INSERT. + */ + const INSERT = 4; + + /** + * Updating existing information in the database, such as SQL's UPDATE. + * + */ + const UPDATE = 8; + + /** + * An operation related to deleting data in the database, + * such as SQL's DELETE. + */ + const DELETE = 16; + + /** + * Retrieving information from the database, such as SQL's SELECT. + */ + const SELECT = 32; + + /** + * Transactional operation, such as start transaction, commit, or rollback. + */ + const TRANSACTION = 64; + + /** + * Inform that a query is stored (in case of filtering) + */ + const STORED = 'stored'; + + /** + * Inform that a query is ignored (in case of filtering) + */ + const IGNORED = 'ignored'; + + /** + * Array of ProfilerQuery objects. + * + * @var array + */ + protected $_queryProfiles = array(); + + /** + * Stores enabled state of the profiler. If set to False, calls to + * queryStart() will simply be ignored. + * + * @var boolean + */ + protected $_enabled = false; + + /** + * Stores the number of seconds to filter. NULL if filtering by time is + * disabled. If an integer is stored here, profiles whose elapsed time + * is less than this value in seconds will be unset from + * the self::$_queryProfiles array. + * + * @var integer + */ + protected $_filterElapsedSecs = null; + + /** + * Logical OR of any of the filter constants. NULL if filtering by query + * type is disable. If an integer is stored here, it is the logical OR of + * any of the query type constants. When the query ends, if it is not + * one of the types specified, it will be unset from the + * self::$_queryProfiles array. + * + * @var integer + */ + protected $_filterTypes = null; + + /** + * Class constructor. The profiler is disabled by default unless it is + * specifically enabled by passing in $enabled here or calling setEnabled(). + * + * @param boolean $enabled + * @return void + */ + public function __construct($enabled = false) + { + $this->setEnabled($enabled); + } + + /** + * Enable or disable the profiler. If $enable is false, the profiler + * is disabled and will not log any queries sent to it. + * + * @param boolean $enable + * @return Profiler Provides a fluent interface + */ + public function setEnabled($enable) + { + $this->_enabled = (boolean) $enable; + + return $this; + } + + /** + * Get the current state of enable. If True is returned, + * the profiler is enabled. + * + * @return boolean + */ + public function getEnabled() + { + return $this->_enabled; + } + + /** + * Sets a minimum number of seconds for saving query profiles. If this + * is set, only those queries whose elapsed time is equal or greater than + * $minimumSeconds will be saved. To save all queries regardless of + * elapsed time, set $minimumSeconds to null. + * + * @param integer $minimumSeconds OPTIONAL + * @return Profiler Provides a fluent interface + */ + public function setFilterElapsedSecs($minimumSeconds = null) + { + if (null === $minimumSeconds) { + $this->_filterElapsedSecs = null; + } else { + $this->_filterElapsedSecs = (integer) $minimumSeconds; + } + + return $this; + } + + /** + * Returns the minimum number of seconds for saving query profiles, or null if + * query profiles are saved regardless of elapsed time. + * + * @return integer|null + */ + public function getFilterElapsedSecs() + { + return $this->_filterElapsedSecs; + } + + /** + * Sets the types of query profiles to save. Set $queryType to one of + * the Zend_Db_Profiler::* constants to only save profiles for that type of + * query. To save more than one type, logical OR them together. To + * save all queries regardless of type, set $queryType to null. + * + * @param integer $queryTypes OPTIONAL + * @return Profiler Provides a fluent interface + */ + public function setFilterQueryType($queryTypes = null) + { + $this->_filterTypes = $queryTypes; + + return $this; + } + + /** + * Returns the types of query profiles saved, or null if queries are saved regardless + * of their types. + * + * @return integer|null + * @see Profiler::setFilterQueryType() + */ + public function getFilterQueryType() + { + return $this->_filterTypes; + } + + /** + * Clears the history of any past query profiles. This is relentless + * and will even clear queries that were started and may not have + * been marked as ended. + * + * @return Profiler Provides a fluent interface + */ + public function clear() + { + $this->_queryProfiles = array(); + + return $this; + } + + /** + * Clone a profiler query + * + * @param ProfilerQuery $query + * @return integer or null + */ + public function queryClone(ProfilerQuery $query) + { + $this->_queryProfiles[] = clone $query; + + end($this->_queryProfiles); + + return key($this->_queryProfiles); + } + + /** + * Starts a query. Creates a new query profile object (ProfilerQuery) + * and returns the "query profiler handle". Run the query, then call + * queryEnd() and pass it this handle to make the query as ended and + * record the time. If the profiler is not enabled, this takes no + * action and immediately returns null. + * + * @param string $queryText SQL statement + * @param integer $queryType OPTIONAL Type of query, one of the Zend_Db_Profiler::* constants + * @return integer|null + */ + public function queryStart($queryText, $queryType = null) + { + if (!$this->_enabled) { + return null; + } + + // make sure we have a query type + if (null === $queryType) { + switch (strtolower(substr(ltrim($queryText), 0, 6))) { + case 'insert': + $queryType = self::INSERT; + break; + case 'update': + $queryType = self::UPDATE; + break; + case 'delete': + $queryType = self::DELETE; + break; + case 'select': + $queryType = self::SELECT; + break; + default: + $queryType = self::QUERY; + break; + } + } + + /** + * @see ProfilerQuery + */ + $this->_queryProfiles[] = new ProfilerQuery($queryText, $queryType); + + end($this->_queryProfiles); + + return key($this->_queryProfiles); + } + + /** + * Ends a query. Pass it the handle that was returned by queryStart(). + * This will mark the query as ended and save the time. + * + * @param integer $queryId + * @throws ProfilerException + * @return string Inform that a query is stored or ignored. + */ + public function queryEnd($queryId) + { + // Don't do anything if the Zend_Db_Profiler is not enabled. + if (!$this->_enabled) { + return self::IGNORED; + } + + // Check for a valid query handle. + if (!isset($this->_queryProfiles[$queryId])) { + /** + * @see ProfilerException + */ + throw new ProfilerException("Profiler has no query with handle '$queryId'."); + } + + $qp = $this->_queryProfiles[$queryId]; + + // Ensure that the query profile has not already ended + if ($qp->hasEnded()) { + /** + * @see ProfilerException + */ + throw new ProfilerException("Query with profiler handle '$queryId' has already ended."); + } + + // End the query profile so that the elapsed time can be calculated. + $qp->end(); + + /** + * If filtering by elapsed time is enabled, only keep the profile if + * it ran for the minimum time. + */ + if (null !== $this->_filterElapsedSecs && $qp->getElapsedSecs() < $this->_filterElapsedSecs) { + unset($this->_queryProfiles[$queryId]); + return self::IGNORED; + } + + /** + * If filtering by query type is enabled, only keep the query if + * it was one of the allowed types. + */ + if (null !== $this->_filterTypes && !($qp->getQueryType() & $this->_filterTypes)) { + unset($this->_queryProfiles[$queryId]); + return self::IGNORED; + } + + return self::STORED; + } + + /** + * Get a profile for a query. Pass it the same handle that was returned + * by queryStart() and it will return a ProfilerQuery object. + * + * @param integer $queryId + * @throws ProfilerException + * @return ProfilerQuery + */ + public function getQueryProfile($queryId) + { + if (!array_key_exists($queryId, $this->_queryProfiles)) { + /** + * @see ProfilerException + */ + throw new ProfilerException("Query handle '$queryId' not found in profiler log."); + } + + return $this->_queryProfiles[$queryId]; + } + + /** + * Get an array of query profiles (ProfilerQuery objects). If $queryType + * is set to one of the Zend_Db_Profiler::* constants then only queries of that + * type will be returned. Normally, queries that have not yet ended will + * not be returned unless $showUnfinished is set to True. If no + * queries were found, False is returned. The returned array is indexed by the query + * profile handles. + * + * @param integer $queryType + * @param boolean $showUnfinished + * @return array|false + */ + public function getQueryProfiles($queryType = null, $showUnfinished = false) + { + $queryProfiles = array(); + foreach ($this->_queryProfiles as $key => $qp) { + if ($queryType === null) { + $condition = true; + } else { + $condition = ($qp->getQueryType() & $queryType); + } + + if (($qp->hasEnded() || $showUnfinished) && $condition) { + $queryProfiles[$key] = $qp; + } + } + + if (empty($queryProfiles)) { + $queryProfiles = false; + } + + return $queryProfiles; + } + + /** + * Get the total elapsed time (in seconds) of all of the profiled queries. + * Only queries that have ended will be counted. If $queryType is set to + * one or more of the Zend_Db_Profiler::* constants, the elapsed time will be calculated + * only for queries of the given type(s). + * + * @param integer $queryType OPTIONAL + * @return float + */ + public function getTotalElapsedSecs($queryType = null) + { + $elapsedSecs = 0; + foreach ($this->_queryProfiles as $key => $qp) { + if (null === $queryType) { + $condition = true; + } else { + $condition = ($qp->getQueryType() & $queryType); + } + if (($qp->hasEnded()) && $condition) { + $elapsedSecs += $qp->getElapsedSecs(); + } + } + return $elapsedSecs; + } + + /** + * Get the total number of queries that have been profiled. Only queries that have ended will + * be counted. If $queryType is set to one of the Zend_Db_Profiler::* constants, only queries of + * that type will be counted. + * + * @param integer $queryType OPTIONAL + * @return integer + */ + public function getTotalNumQueries($queryType = null) + { + if (null === $queryType) { + return count($this->_queryProfiles); + } + + $numQueries = 0; + foreach ($this->_queryProfiles as $qp) { + if ($qp->hasEnded() && ($qp->getQueryType() & $queryType)) { + $numQueries++; + } + } + + return $numQueries; + } + + /** + * Get the ProfilerQuery object for the last query that was run, regardless if it has + * ended or not. If the query has not ended, its end time will be null. If no queries have + * been profiled, false is returned. + * + * @return ProfilerQuery|false + */ + public function getLastQueryProfile() + { + if (empty($this->_queryProfiles)) { + return false; + } + + end($this->_queryProfiles); + + return current($this->_queryProfiles); + } +} diff --git a/vendor/gipfl/zfdb/src/Profiler/ProfilerException.php b/vendor/gipfl/zfdb/src/Profiler/ProfilerException.php new file mode 100644 index 0000000..3722793 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Profiler/ProfilerException.php @@ -0,0 +1,29 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Profiler; + +use gipfl\ZfDb\Exception\DbException; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class ProfilerException extends DbException +{ +} diff --git a/vendor/gipfl/zfdb/src/Profiler/ProfilerQuery.php b/vendor/gipfl/zfdb/src/Profiler/ProfilerQuery.php new file mode 100644 index 0000000..7f79396 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Profiler/ProfilerQuery.php @@ -0,0 +1,206 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Profiler; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class ProfilerQuery +{ + + /** + * SQL query string or user comment, set by $query argument in constructor. + * + * @var string + */ + protected $_query = ''; + + /** + * One of the Zend_Db_Profiler constants for query type, set by $queryType argument in constructor. + * + * @var integer + */ + protected $_queryType = 0; + + /** + * Unix timestamp with microseconds when instantiated. + * + * @var float + */ + protected $_startedMicrotime = null; + + /** + * Unix timestamp with microseconds when self::queryEnd() was called. + * + * @var integer + */ + protected $_endedMicrotime = null; + + /** + * @var array + */ + protected $_boundParams = array(); + + /** + * @var array + */ + + /** + * Class constructor. A query is about to be started, save the query text ($query) and its + * type (one of the Zend_Db_Profiler::* constants). + * + * @param string $query + * @param integer $queryType + * @return void + */ + public function __construct($query, $queryType) + { + $this->_query = $query; + $this->_queryType = $queryType; + // by default, and for backward-compatibility, start the click ticking + $this->start(); + } + + /** + * Clone handler for the query object. + * @return void + */ + public function __clone() + { + $this->_boundParams = array(); + $this->_endedMicrotime = null; + $this->start(); + } + + /** + * Starts the elapsed time click ticking. + * This can be called subsequent to object creation, + * to restart the clock. For instance, this is useful + * right before executing a prepared query. + * + * @return void + */ + public function start() + { + $this->_startedMicrotime = microtime(true); + } + + /** + * Ends the query and records the time so that the elapsed time can be determined later. + * + * @return void + */ + public function end() + { + $this->_endedMicrotime = microtime(true); + } + + /** + * Returns true if and only if the query has ended. + * + * @return boolean + */ + public function hasEnded() + { + return $this->_endedMicrotime !== null; + } + + /** + * Get the original SQL text of the query. + * + * @return string + */ + public function getQuery() + { + return $this->_query; + } + + /** + * Get the type of this query (one of the Zend_Db_Profiler::* constants) + * + * @return integer + */ + public function getQueryType() + { + return $this->_queryType; + } + + /** + * @param string $param + * @param mixed $variable + * @return void + */ + public function bindParam($param, $variable) + { + $this->_boundParams[$param] = $variable; + } + + /** + * @param array $param + * @return void + */ + public function bindParams(array $params) + { + if (array_key_exists(0, $params)) { + array_unshift($params, null); + unset($params[0]); + } + foreach ($params as $param => $value) { + $this->bindParam($param, $value); + } + } + + /** + * @return array + */ + public function getQueryParams() + { + return $this->_boundParams; + } + + /** + * Get the elapsed time (in seconds) that the query ran. + * If the query has not yet ended, false is returned. + * + * @return float|false + */ + public function getElapsedSecs() + { + if (null === $this->_endedMicrotime) { + return false; + } + + return $this->_endedMicrotime - $this->_startedMicrotime; + } + + /** + * Get the time (in seconds) when the profiler started running. + * + * @return bool|float + */ + public function getStartedMicrotime() + { + if (null === $this->_startedMicrotime) { + return false; + } + + return $this->_startedMicrotime; + } +} diff --git a/vendor/gipfl/zfdb/src/Select.php b/vendor/gipfl/zfdb/src/Select.php new file mode 100644 index 0000000..3a66e86 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Select.php @@ -0,0 +1,1350 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb; + +use gipfl\ZfDb\Adapter\Adapter; +use gipfl\ZfDb\Exception\DbException; +use gipfl\ZfDb\Exception\SelectException; +use PDOStatement; + +/** + * Class for SQL SELECT generation and results. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Select +{ + const DISTINCT = 'distinct'; + const COLUMNS = 'columns'; + const FROM = 'from'; + const UNION = 'union'; + const WHERE = 'where'; + const GROUP = 'group'; + const HAVING = 'having'; + const ORDER = 'order'; + const LIMIT_COUNT = 'limitcount'; + const LIMIT_OFFSET = 'limitoffset'; + const FOR_UPDATE = 'forupdate'; + + const INNER_JOIN = 'inner join'; + const LEFT_JOIN = 'left join'; + const RIGHT_JOIN = 'right join'; + const FULL_JOIN = 'full join'; + const CROSS_JOIN = 'cross join'; + const NATURAL_JOIN = 'natural join'; + + const SQL_WILDCARD = '*'; + const SQL_SELECT = 'SELECT'; + const SQL_UNION = 'UNION'; + const SQL_UNION_ALL = 'UNION ALL'; + const SQL_FROM = 'FROM'; + const SQL_WHERE = 'WHERE'; + const SQL_DISTINCT = 'DISTINCT'; + const SQL_GROUP_BY = 'GROUP BY'; + const SQL_ORDER_BY = 'ORDER BY'; + const SQL_HAVING = 'HAVING'; + const SQL_FOR_UPDATE = 'FOR UPDATE'; + const SQL_AND = 'AND'; + const SQL_AS = 'AS'; + const SQL_OR = 'OR'; + const SQL_ON = 'ON'; + const SQL_ASC = 'ASC'; + const SQL_DESC = 'DESC'; + + const REGEX_COLUMN_EXPR = '/^([\w]*\s*\(([^\(\)]|(?1))*\))$/'; + const REGEX_COLUMN_EXPR_ORDER = '/^([\w]+\s*\(([^\(\)]|(?1))*\))$/'; + const REGEX_COLUMN_EXPR_GROUP = '/^([\w]+\s*\(([^\(\)]|(?1))*\))$/'; + + // @see http://stackoverflow.com/a/13823184/2028814 + const REGEX_SQL_COMMENTS = '@ + (([\'"]).*?[^\\\]\2) # $1 : Skip single & double quoted expressions + |( # $3 : Match comments + (?:\#|--).*?$ # - Single line comments + | # - Multi line (nested) comments + /\* # . comment open marker + (?: [^/*] # . non comment-marker characters + |/(?!\*) # . ! not a comment open + |\*(?!/) # . ! not a comment close + |(?R) # . recursive case + )* # . repeat eventually + \*\/ # . comment close marker + )\s* # Trim after comments + |(?<=;)\s+ # Trim after semi-colon + @msx'; + + /** + * Bind variables for query + * + * @var array + */ + protected $_bind = array(); + + /** + * Zend_Db_Adapter_Abstract object. + * + * @var Adapter + */ + protected $_adapter; + + /** + * The initial values for the $_parts array. + * NOTE: It is important for the 'FOR_UPDATE' part to be last to ensure + * meximum compatibility with database adapters. + * + * @var array + */ + protected static $_partsInit = array( + self::DISTINCT => false, + self::COLUMNS => array(), + self::UNION => array(), + self::FROM => array(), + self::WHERE => array(), + self::GROUP => array(), + self::HAVING => array(), + self::ORDER => array(), + self::LIMIT_COUNT => null, + self::LIMIT_OFFSET => null, + self::FOR_UPDATE => false + ); + + /** + * Specify legal join types. + * + * @var array + */ + protected static $_joinTypes = array( + self::INNER_JOIN, + self::LEFT_JOIN, + self::RIGHT_JOIN, + self::FULL_JOIN, + self::CROSS_JOIN, + self::NATURAL_JOIN, + ); + + /** + * Specify legal union types. + * + * @var array + */ + protected static $_unionTypes = array( + self::SQL_UNION, + self::SQL_UNION_ALL + ); + + /** + * The component parts of a SELECT statement. + * Initialized to the $_partsInit array in the constructor. + * + * @var array + */ + protected $_parts = array(); + + /** + * Tracks which columns are being select from each table and join. + * + * @var array + */ + protected $_tableCols = array(); + + /** + * Class constructor + * + * @param Adapter $adapter + */ + public function __construct(Adapter $adapter) + { + $this->_adapter = $adapter; + $this->_parts = self::$_partsInit; + } + + /** + * Get bind variables + * + * @return array + */ + public function getBind() + { + return $this->_bind; + } + + /** + * Set bind variables + * + * @param mixed $bind + * @return Select + */ + public function bind($bind) + { + $this->_bind = $bind; + + return $this; + } + + /** + * Makes the query SELECT DISTINCT. + * + * @param bool $flag Whether or not the SELECT is DISTINCT (default true). + * @return Select This Zend_Db_Select object. + */ + public function distinct($flag = true) + { + $this->_parts[self::DISTINCT] = (bool) $flag; + return $this; + } + + /** + * Adds a FROM table and optional columns to the query. + * + * The first parameter $name can be a simple string, in which case the + * correlation name is generated automatically. If you want to specify + * the correlation name, the first parameter must be an associative + * array in which the key is the correlation name, and the value is + * the physical table name. For example, array('alias' => 'table'). + * The correlation name is prepended to all columns fetched for this + * table. + * + * The second parameter can be a single string or Zend_Db_Expr object, + * or else an array of strings or Zend_Db_Expr objects. + * + * The first parameter can be null or an empty string, in which case + * no correlation name is generated or prepended to the columns named + * in the second parameter. + * + * @param array|string|Expr $name The table name or an associative array + * relating correlation name to table name. + * @param array|string|Expr $cols The columns to select from this table. + * @param string $schema The schema name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function from($name, $cols = '*', $schema = null) + { + return $this->_join(self::FROM, $name, null, $cols, $schema); + } + + /** + * Specifies the columns used in the FROM clause. + * + * The parameter can be a single string or Zend_Db_Expr object, + * or else an array of strings or Zend_Db_Expr objects. + * + * @param array|string|Expr $cols The columns to select from this table. + * @param string $correlationName Correlation name of target table. OPTIONAL + * @return Select This Zend_Db_Select object. + */ + public function columns($cols = '*', $correlationName = null) + { + if ($correlationName === null && count($this->_parts[self::FROM])) { + $correlationNameKeys = array_keys($this->_parts[self::FROM]); + $correlationName = current($correlationNameKeys); + } + + if (!array_key_exists($correlationName, $this->_parts[self::FROM])) { + throw new SelectException("No table has been specified for the FROM clause"); + } + + $this->_tableCols($correlationName, $cols); + + return $this; + } + + /** + * Adds a UNION clause to the query. + * + * The first parameter has to be an array of Zend_Db_Select or + * sql query strings. + * + * <code> + * $sql1 = $db->select(); + * $sql2 = "SELECT ..."; + * $select = $db->select() + * ->union(array($sql1, $sql2)) + * ->order("id"); + * </code> + * + * @param array $select Array of select clauses for the union. + * @return Select This Zend_Db_Select object. + */ + public function union($select = array(), $type = self::SQL_UNION) + { + if (!is_array($select)) { + throw new SelectException( + "union() only accepts an array of Zend_Db_Select instances of sql query strings." + ); + } + + if (!in_array($type, self::$_unionTypes)) { + throw new SelectException("Invalid union type '{$type}'"); + } + + foreach ($select as $target) { + $this->_parts[self::UNION][] = array($target, $type); + } + + return $this; + } + + /** + * Adds a JOIN table and columns to the query. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param array|string|Expr $name The table name. + * @param string $cond Join on this condition. + * @param array|string $cols The columns to select from the joined table. + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function join($name, $cond, $cols = self::SQL_WILDCARD, $schema = null) + { + return $this->joinInner($name, $cond, $cols, $schema); + } + + /** + * Add an INNER JOIN table and colums to the query + * Rows in both tables are matched according to the expression + * in the $cond argument. The result set is comprised + * of all cases where rows from the left table match + * rows from the right table. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param array|string|Expr $name The table name. + * @param string $cond Join on this condition. + * @param array|string $cols The columns to select from the joined table. + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function joinInner($name, $cond, $cols = self::SQL_WILDCARD, $schema = null) + { + return $this->_join(self::INNER_JOIN, $name, $cond, $cols, $schema); + } + + /** + * Add a LEFT OUTER JOIN table and colums to the query + * All rows from the left operand table are included, + * matching rows from the right operand table included, + * and the columns from the right operand table are filled + * with NULLs if no row exists matching the left table. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param array|string|Expr $name The table name. + * @param string $cond Join on this condition. + * @param array|string $cols The columns to select from the joined table. + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function joinLeft($name, $cond, $cols = self::SQL_WILDCARD, $schema = null) + { + return $this->_join(self::LEFT_JOIN, $name, $cond, $cols, $schema); + } + + /** + * Add a RIGHT OUTER JOIN table and colums to the query. + * Right outer join is the complement of left outer join. + * All rows from the right operand table are included, + * matching rows from the left operand table included, + * and the columns from the left operand table are filled + * with NULLs if no row exists matching the right table. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param array|string|Expr $name The table name. + * @param string $cond Join on this condition. + * @param array|string $cols The columns to select from the joined table. + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function joinRight($name, $cond, $cols = self::SQL_WILDCARD, $schema = null) + { + return $this->_join(self::RIGHT_JOIN, $name, $cond, $cols, $schema); + } + + /** + * Add a FULL OUTER JOIN table and colums to the query. + * A full outer join is like combining a left outer join + * and a right outer join. All rows from both tables are + * included, paired with each other on the same row of the + * result set if they satisfy the join condition, and otherwise + * paired with NULLs in place of columns from the other table. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param array|string|Expr $name The table name. + * @param string $cond Join on this condition. + * @param array|string $cols The columns to select from the joined table. + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function joinFull($name, $cond, $cols = self::SQL_WILDCARD, $schema = null) + { + return $this->_join(self::FULL_JOIN, $name, $cond, $cols, $schema); + } + + /** + * Add a CROSS JOIN table and colums to the query. + * A cross join is a cartesian product; there is no join condition. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param array|string|Expr $name The table name. + * @param array|string $cols The columns to select from the joined table. + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function joinCross($name, $cols = self::SQL_WILDCARD, $schema = null) + { + return $this->_join(self::CROSS_JOIN, $name, null, $cols, $schema); + } + + /** + * Add a NATURAL JOIN table and colums to the query. + * A natural join assumes an equi-join across any column(s) + * that appear with the same name in both tables. + * Only natural inner joins are supported by this API, + * even though SQL permits natural outer joins as well. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param array|string|Expr $name The table name. + * @param array|string $cols The columns to select from the joined table. + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object. + */ + public function joinNatural($name, $cols = self::SQL_WILDCARD, $schema = null) + { + return $this->_join(self::NATURAL_JOIN, $name, null, $cols, $schema); + } + + /** + * Adds a WHERE condition to the query by AND. + * + * If a value is passed as the second param, it will be quoted + * and replaced into the condition wherever a question-mark + * appears. Array values are quoted and comma-separated. + * + * <code> + * // simplest but non-secure + * $select->where("id = $id"); + * + * // secure (ID is quoted but matched anyway) + * $select->where('id = ?', $id); + * + * // alternatively, with named binding + * $select->where('id = :id'); + * </code> + * + * Note that it is more correct to use named bindings in your + * queries for values other than strings. When you use named + * bindings, don't forget to pass the values when actually + * making a query: + * + * <code> + * $db->fetchAll($select, array('id' => 5)); + * </code> + * + * @param string $cond The WHERE condition. + * @param mixed $value OPTIONAL The value to quote into the condition. + * @param int $type OPTIONAL The type of the given value + * @return Select This Zend_Db_Select object. + */ + public function where($cond, $value = null, $type = null) + { + $this->_parts[self::WHERE][] = $this->_where($cond, $value, $type, true); + + return $this; + } + + /** + * Adds a WHERE condition to the query by OR. + * + * Otherwise identical to where(). + * + * @param string $cond The WHERE condition. + * @param mixed $value OPTIONAL The value to quote into the condition. + * @param int $type OPTIONAL The type of the given value + * @return Select This Zend_Db_Select object. + * + * @see where() + */ + public function orWhere($cond, $value = null, $type = null) + { + $this->_parts[self::WHERE][] = $this->_where($cond, $value, $type, false); + + return $this; + } + + /** + * Adds grouping to the query. + * + * @param array|string $spec The column(s) to group by. + * @return Select This Zend_Db_Select object. + */ + public function group($spec) + { + if (!is_array($spec)) { + $spec = array($spec); + } + + foreach ($spec as $val) { + // Remove comments from SQL statement + $noComments = preg_replace(self::REGEX_SQL_COMMENTS, '$1', (string) $val); + if (preg_match(self::REGEX_COLUMN_EXPR_GROUP, $noComments)) { + $val = new Expr($val); + } + $this->_parts[self::GROUP][] = $val; + } + + return $this; + } + + /** + * Adds a HAVING condition to the query by AND. + * + * If a value is passed as the second param, it will be quoted + * and replaced into the condition wherever a question-mark + * appears. See {@link where()} for an example + * + * @param string $cond The HAVING condition. + * @param mixed $value OPTIONAL The value to quote into the condition. + * @param int $type OPTIONAL The type of the given value + * @return Select This Zend_Db_Select object. + */ + public function having($cond, $value = null, $type = null) + { + if ($value !== null) { + $cond = $this->_adapter->quoteInto($cond, $value, $type); + } + + if ($this->_parts[self::HAVING]) { + $this->_parts[self::HAVING][] = self::SQL_AND . " ($cond)"; + } else { + $this->_parts[self::HAVING][] = "($cond)"; + } + + return $this; + } + + /** + * Adds a HAVING condition to the query by OR. + * + * Otherwise identical to orHaving(). + * + * @param string $cond The HAVING condition. + * @param mixed $value OPTIONAL The value to quote into the condition. + * @param int $type OPTIONAL The type of the given value + * @return Select This Zend_Db_Select object. + * + * @see having() + */ + public function orHaving($cond, $value = null, $type = null) + { + if ($value !== null) { + $cond = $this->_adapter->quoteInto($cond, $value, $type); + } + + if ($this->_parts[self::HAVING]) { + $this->_parts[self::HAVING][] = self::SQL_OR . " ($cond)"; + } else { + $this->_parts[self::HAVING][] = "($cond)"; + } + + return $this; + } + + /** + * Adds a row order to the query. + * + * @param mixed $spec The column(s) and direction to order by. + * @return Select This Zend_Db_Select object. + */ + public function order($spec) + { + if (!is_array($spec)) { + $spec = array($spec); + } + + // force 'ASC' or 'DESC' on each order spec, default is ASC. + foreach ($spec as $val) { + if ($val instanceof Expr) { + $expr = $val->__toString(); + if (empty($expr)) { + continue; + } + $this->_parts[self::ORDER][] = $val; + } else { + if (empty($val)) { + continue; + } + $direction = self::SQL_ASC; + if (preg_match('/(.*\W)(' . self::SQL_ASC . '|' . self::SQL_DESC . ')\b/si', $val, $matches)) { + $val = trim($matches[1]); + $direction = $matches[2]; + } + // Remove comments from SQL statement + $noComments = preg_replace(self::REGEX_SQL_COMMENTS, '$1', (string) $val); + if (preg_match(self::REGEX_COLUMN_EXPR_ORDER, $noComments)) { + $val = new Expr($val); + } + $this->_parts[self::ORDER][] = array($val, $direction); + } + } + + return $this; + } + + /** + * Sets a limit count and offset to the query. + * + * @param int $count OPTIONAL The number of rows to return. + * @param int $offset OPTIONAL Start returning after this many rows. + * @return Select This Zend_Db_Select object. + */ + public function limit($count = null, $offset = null) + { + $this->_parts[self::LIMIT_COUNT] = (int) $count; + $this->_parts[self::LIMIT_OFFSET] = (int) $offset; + return $this; + } + + /** + * Sets the limit and count by page number. + * + * @param int $page Limit results to this page number. + * @param int $rowCount Use this many rows per page. + * @return Select This Zend_Db_Select object. + */ + public function limitPage($page, $rowCount) + { + $page = ($page > 0) ? $page : 1; + $rowCount = ($rowCount > 0) ? $rowCount : 1; + $this->_parts[self::LIMIT_COUNT] = (int) $rowCount; + $this->_parts[self::LIMIT_OFFSET] = (int) $rowCount * ($page - 1); + return $this; + } + + /** + * Makes the query SELECT FOR UPDATE. + * + * @param bool $flag Whether or not the SELECT is FOR UPDATE (default true). + * @return Select This Zend_Db_Select object. + */ + public function forUpdate($flag = true) + { + $this->_parts[self::FOR_UPDATE] = (bool) $flag; + return $this; + } + + /** + * Get part of the structured information for the current query. + * + * @param string $part + * @return mixed + * @throws SelectException + */ + public function getPart($part) + { + $part = strtolower($part); + if (!array_key_exists($part, $this->_parts)) { + throw new SelectException("Invalid Select part '$part'"); + } + return $this->_parts[$part]; + } + + /** + * Executes the current select object and returns the result + * + * @param integer $fetchMode OPTIONAL + * @param mixed $bind An array of data to bind to the placeholders. + * @return PDOStatement|Statement + */ + public function query($fetchMode = null, $bind = array()) + { + if (!empty($bind)) { + $this->bind($bind); + } + + $stmt = $this->_adapter->query($this); + if ($fetchMode == null) { + $fetchMode = $this->_adapter->getFetchMode(); + } + $stmt->setFetchMode($fetchMode); + return $stmt; + } + + /** + * Converts this object to an SQL SELECT string. + * + * @return string|null This object as a SELECT string. (or null if a string cannot be produced.) + * @throws SelectException + */ + public function assemble() + { + $sql = self::SQL_SELECT; + foreach (array_keys(self::$_partsInit) as $part) { + $method = '_render' . ucfirst($part); + if (method_exists($this, $method)) { + $sql = $this->$method($sql); + } + } + return $sql; + } + + /** + * Clear parts of the Select object, or an individual part. + * + * @param string $part OPTIONAL + * @return Select + */ + public function reset($part = null) + { + if ($part == null) { + $this->_parts = self::$_partsInit; + } elseif (array_key_exists($part, self::$_partsInit)) { + $this->_parts[$part] = self::$_partsInit[$part]; + } + return $this; + } + + /** + * Gets the Zend_Db_Adapter_Abstract for this + * particular Zend_Db_Select object. + * + * @return Adapter + */ + public function getAdapter() + { + return $this->_adapter; + } + + /** + * Populate the {@link $_parts} 'join' key + * + * Does the dirty work of populating the join key. + * + * The $name and $cols parameters follow the same logic + * as described in the from() method. + * + * @param null|string $type Type of join; inner, left, and null are currently supported + * @param array|string|Expr $name Table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any. + * @return Select This Zend_Db_Select object + * @throws SelectException + */ + protected function _join($type, $name, $cond, $cols, $schema = null) + { + if (!in_array($type, self::$_joinTypes) && $type != self::FROM) { + throw new SelectException("Invalid join type '$type'"); + } + + if (count($this->_parts[self::UNION])) { + throw new SelectException("Invalid use of table with " . self::SQL_UNION); + } + + if (empty($name)) { + $correlationName = $tableName = ''; + } elseif (is_array($name)) { + // Must be array($correlationName => $tableName) or array($ident, ...) + foreach ($name as $_correlationName => $_tableName) { + if (is_string($_correlationName)) { + // We assume the key is the correlation name and value is the table name + $tableName = $_tableName; + $correlationName = $_correlationName; + } else { + // We assume just an array of identifiers, with no correlation name + $tableName = $_tableName; + $correlationName = $this->_uniqueCorrelation($tableName); + } + break; + } + } elseif ($name instanceof Expr|| $name instanceof Select) { + $tableName = $name; + $correlationName = $this->_uniqueCorrelation('t'); + } elseif (preg_match('/^(.+)\s+AS\s+(.+)$/i', $name, $m)) { + $tableName = $m[1]; + $correlationName = $m[2]; + } else { + $tableName = $name; + $correlationName = $this->_uniqueCorrelation($tableName); + } + + // Schema from table name overrides schema argument + if (!is_object($tableName) && false !== strpos($tableName, '.')) { + list($schema, $tableName) = explode('.', $tableName); + } + + $lastFromCorrelationName = null; + if (!empty($correlationName)) { + if (array_key_exists($correlationName, $this->_parts[self::FROM])) { + /** + * @see SelectException + */ + throw new SelectException("You cannot define a correlation name '$correlationName' more than once"); + } + + if ($type == self::FROM) { + // append this from after the last from joinType + $tmpFromParts = $this->_parts[self::FROM]; + $this->_parts[self::FROM] = array(); + // move all the froms onto the stack + while ($tmpFromParts) { + $currentCorrelationName = key($tmpFromParts); + if ($tmpFromParts[$currentCorrelationName]['joinType'] != self::FROM) { + break; + } + $lastFromCorrelationName = $currentCorrelationName; + $this->_parts[self::FROM][$currentCorrelationName] = array_shift($tmpFromParts); + } + } else { + $tmpFromParts = array(); + } + $this->_parts[self::FROM][$correlationName] = array( + 'joinType' => $type, + 'schema' => $schema, + 'tableName' => $tableName, + 'joinCondition' => $cond + ); + while ($tmpFromParts) { + $currentCorrelationName = key($tmpFromParts); + $this->_parts[self::FROM][$currentCorrelationName] = array_shift($tmpFromParts); + } + } + + // add to the columns from this joined table + if ($type == self::FROM && $lastFromCorrelationName == null) { + $lastFromCorrelationName = true; + } + $this->_tableCols($correlationName, $cols, $lastFromCorrelationName); + + return $this; + } + + /** + * Handle JOIN... USING... syntax + * + * This is functionality identical to the existing JOIN methods, however + * the join condition can be passed as a single column name. This method + * then completes the ON condition by using the same field for the FROM + * table and the JOIN table. + * + * <code> + * $select = $db->select()->from('table1') + * ->joinUsing('table2', 'column1'); + * + * // SELECT * FROM table1 JOIN table2 ON table1.column1 = table2.column2 + * </code> + * + * These joins are called by the developer simply by adding 'Using' to the + * method name. E.g. + * * joinUsing + * * joinInnerUsing + * * joinFullUsing + * * joinRightUsing + * * joinLeftUsing + * + * @return Select This Zend_Db_Select object. + */ + public function _joinUsing($type, $name, $cond, $cols = '*', $schema = null) + { + if (empty($this->_parts[self::FROM])) { + throw new SelectException("You can only perform a joinUsing after specifying a FROM table"); + } + + $join = $this->_adapter->quoteIdentifier(key($this->_parts[self::FROM]), true); + $from = $this->_adapter->quoteIdentifier($this->_uniqueCorrelation($name), true); + + $joinCond = array(); + foreach ((array)$cond as $fieldName) { + $cond1 = $from . '.' . $fieldName; + $cond2 = $join . '.' . $fieldName; + $joinCond[] = $cond1 . ' = ' . $cond2; + } + $cond = implode(' '.self::SQL_AND.' ', $joinCond); + + return $this->_join($type, $name, $cond, $cols, $schema); + } + + /** + * Generate a unique correlation name + * + * @param string|array $name A qualified identifier. + * @return string A unique correlation name. + */ + private function _uniqueCorrelation($name) + { + if (is_array($name)) { + $k = key($name); + $c = is_string($k) ? $k : end($name); + } else { + // Extract just the last name of a qualified table name + $dot = strrpos($name, '.'); + $c = ($dot === false) ? $name : substr($name, $dot+1); + } + for ($i = 2; array_key_exists($c, $this->_parts[self::FROM]); ++$i) { + $c = $name . '_' . (string) $i; + } + return $c; + } + + /** + * Adds to the internal table-to-column mapping array. + * + * @param string $tbl The table/join the columns come from. + * @param array|string $cols The list of columns; preferably as + * an array, but possibly as a string containing one column. + * @param bool|string True if it should be prepended, a correlation name if it should be inserted + * @return void + */ + protected function _tableCols($correlationName, $cols, $afterCorrelationName = null) + { + if (!is_array($cols)) { + $cols = array($cols); + } + + if ($correlationName == null) { + $correlationName = ''; + } + + $columnValues = array(); + + foreach (array_filter($cols) as $alias => $col) { + $currentCorrelationName = $correlationName; + if (is_string($col)) { + // Check for a column matching "<column> AS <alias>" and extract the alias name + $col = trim(str_replace("\n", ' ', $col)); + if (preg_match('/^(.+)\s+' . self::SQL_AS . '\s+(.+)$/i', $col, $m)) { + $col = $m[1]; + $alias = $m[2]; + } + // Check for columns that look like functions and convert to Zend_Db_Expr + if (preg_match(self::REGEX_COLUMN_EXPR, (string) $col)) { + $col = new Expr($col); + } elseif (preg_match('/(.+)\.(.+)/', $col, $m)) { + $currentCorrelationName = $m[1]; + $col = $m[2]; + } + } + $columnValues[] = array($currentCorrelationName, $col, is_string($alias) ? $alias : null); + } + + if ($columnValues) { + // should we attempt to prepend or insert these values? + if ($afterCorrelationName === true || is_string($afterCorrelationName)) { + $tmpColumns = $this->_parts[self::COLUMNS]; + $this->_parts[self::COLUMNS] = array(); + } else { + $tmpColumns = array(); + } + + // find the correlation name to insert after + if (is_string($afterCorrelationName)) { + while ($tmpColumns) { + $this->_parts[self::COLUMNS][] = $currentColumn = array_shift($tmpColumns); + if ($currentColumn[0] == $afterCorrelationName) { + break; + } + } + } + + // apply current values to current stack + foreach ($columnValues as $columnValue) { + array_push($this->_parts[self::COLUMNS], $columnValue); + } + + // finish ensuring that all previous values are applied (if they exist) + while ($tmpColumns) { + array_push($this->_parts[self::COLUMNS], array_shift($tmpColumns)); + } + } + } + + /** + * Internal function for creating the where clause + * + * @param string $condition + * @param mixed $value optional + * @param string $type optional + * @param boolean $bool true = AND, false = OR + * @return string clause + */ + protected function _where($condition, $value = null, $type = null, $bool = true) + { + if (count($this->_parts[self::UNION])) { + throw new SelectException("Invalid use of where clause with " . self::SQL_UNION); + } + + if ($value !== null) { + $condition = $this->_adapter->quoteInto($condition, $value, $type); + } + + $cond = ""; + if ($this->_parts[self::WHERE]) { + if ($bool === true) { + $cond = self::SQL_AND . ' '; + } else { + $cond = self::SQL_OR . ' '; + } + } + + return $cond . "($condition)"; + } + + /** + * @return array + */ + protected function _getDummyTable() + { + return array(); + } + + /** + * Return a quoted schema name + * + * @param string $schema The schema name OPTIONAL + * @return string|null + */ + protected function _getQuotedSchema($schema = null) + { + if ($schema === null) { + return null; + } + return $this->_adapter->quoteIdentifier($schema, true) . '.'; + } + + /** + * Return a quoted table name + * + * @param string $tableName The table name + * @param string $correlationName The correlation name OPTIONAL + * @return string + */ + protected function _getQuotedTable($tableName, $correlationName = null) + { + return $this->_adapter->quoteTableAs($tableName, $correlationName, true); + } + + /** + * Render DISTINCT clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderDistinct($sql) + { + if ($this->_parts[self::DISTINCT]) { + $sql .= ' ' . self::SQL_DISTINCT; + } + + return $sql; + } + + /** + * Render DISTINCT clause + * + * @param string $sql SQL query + * @return string|null + */ + protected function _renderColumns($sql) + { + if (!count($this->_parts[self::COLUMNS])) { + return null; + } + + $columns = array(); + foreach ($this->_parts[self::COLUMNS] as $columnEntry) { + list($correlationName, $column, $alias) = $columnEntry; + if ($column instanceof Expr) { + $columns[] = $this->_adapter->quoteColumnAs($column, $alias, true); + } else { + if ($column == self::SQL_WILDCARD) { + $column = new Expr(self::SQL_WILDCARD); + $alias = null; + } + if (empty($correlationName)) { + $columns[] = $this->_adapter->quoteColumnAs($column, $alias, true); + } else { + $columns[] = $this->_adapter->quoteColumnAs(array($correlationName, $column), $alias, true); + } + } + } + + return $sql . ' ' . implode(', ', $columns); + } + + /** + * Render FROM clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderFrom($sql) + { + /* + * If no table specified, use RDBMS-dependent solution + * for table-less query. e.g. DUAL in Oracle. + */ + if (empty($this->_parts[self::FROM])) { + $this->_parts[self::FROM] = $this->_getDummyTable(); + } + + $from = array(); + + foreach ($this->_parts[self::FROM] as $correlationName => $table) { + $tmp = ''; + + $joinType = ($table['joinType'] == self::FROM) ? self::INNER_JOIN : $table['joinType']; + + // Add join clause (if applicable) + if (! empty($from)) { + $tmp .= ' ' . strtoupper($joinType) . ' '; + } + + $tmp .= $this->_getQuotedSchema($table['schema']); + $tmp .= $this->_getQuotedTable($table['tableName'], $correlationName); + + // Add join conditions (if applicable) + if (!empty($from) && ! empty($table['joinCondition'])) { + $tmp .= ' ' . self::SQL_ON . ' ' . $table['joinCondition']; + } + + // Add the table name and condition add to the list + $from[] = $tmp; + } + + // Add the list of all joins + if (!empty($from)) { + $sql .= ' ' . self::SQL_FROM . ' ' . implode("\n", $from); + } + + return $sql; + } + + /** + * Render UNION query + * + * @param string $sql SQL query + * @return string + */ + protected function _renderUnion($sql) + { + if ($this->_parts[self::UNION]) { + $parts = count($this->_parts[self::UNION]); + foreach ($this->_parts[self::UNION] as $cnt => $union) { + list($target, $type) = $union; + if ($target instanceof Select) { + $target = $target->assemble(); + } + $sql .= $target; + if ($cnt < $parts - 1) { + $sql .= ' ' . $type . ' '; + } + } + } + + return $sql; + } + + /** + * Render WHERE clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderWhere($sql) + { + if ($this->_parts[self::FROM] && $this->_parts[self::WHERE]) { + $sql .= ' ' . self::SQL_WHERE . ' ' . implode(' ', $this->_parts[self::WHERE]); + } + + return $sql; + } + + /** + * Render GROUP clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderGroup($sql) + { + if ($this->_parts[self::FROM] && $this->_parts[self::GROUP]) { + $group = array(); + foreach ($this->_parts[self::GROUP] as $term) { + $group[] = $this->_adapter->quoteIdentifier($term, true); + } + $sql .= ' ' . self::SQL_GROUP_BY . ' ' . implode(",\n\t", $group); + } + + return $sql; + } + + /** + * Render HAVING clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderHaving($sql) + { + if ($this->_parts[self::FROM] && $this->_parts[self::HAVING]) { + $sql .= ' ' . self::SQL_HAVING . ' ' . implode(' ', $this->_parts[self::HAVING]); + } + + return $sql; + } + + /** + * Render ORDER clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderOrder($sql) + { + if ($this->_parts[self::ORDER]) { + $order = array(); + foreach ($this->_parts[self::ORDER] as $term) { + if (is_array($term)) { + if (is_numeric($term[0]) && strval(intval($term[0])) == $term[0]) { + $order[] = (int)trim($term[0]) . ' ' . $term[1]; + } else { + $order[] = $this->_adapter->quoteIdentifier($term[0], true) . ' ' . $term[1]; + } + } elseif (is_numeric($term) && strval(intval($term)) == $term) { + $order[] = (int)trim($term); + } else { + $order[] = $this->_adapter->quoteIdentifier($term, true); + } + } + $sql .= ' ' . self::SQL_ORDER_BY . ' ' . implode(', ', $order); + } + + return $sql; + } + + /** + * Render LIMIT OFFSET clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderLimitoffset($sql) + { + $count = 0; + $offset = 0; + + if (!empty($this->_parts[self::LIMIT_OFFSET])) { + $offset = (int) $this->_parts[self::LIMIT_OFFSET]; + $count = PHP_INT_MAX; + } + + if (!empty($this->_parts[self::LIMIT_COUNT])) { + $count = (int) $this->_parts[self::LIMIT_COUNT]; + } + + /* + * Add limits clause + */ + if ($count > 0) { + $sql = trim($this->_adapter->limit($sql, $count, $offset)); + } + + return $sql; + } + + /** + * Render FOR UPDATE clause + * + * @param string $sql SQL query + * @return string + */ + protected function _renderForupdate($sql) + { + if ($this->_parts[self::FOR_UPDATE]) { + $sql .= ' ' . self::SQL_FOR_UPDATE; + } + + return $sql; + } + + /** + * Turn magic function calls into non-magic function calls + * for joinUsing syntax + * + * @param string $method + * @param array $args OPTIONAL Zend_Db_Table_Select query modifier + * @return Select + * @throws SelectException If an invalid method is called. + */ + public function __call($method, array $args) + { + $matches = array(); + + /** + * Recognize methods for Has-Many cases: + * findParent<Class>() + * findParent<Class>By<Rule>() + * Use the non-greedy pattern repeat modifier e.g. \w+? + */ + if (preg_match('/^join([a-zA-Z]*?)Using$/', $method, $matches)) { + $type = strtolower($matches[1]); + if ($type) { + $type .= ' join'; + if (!in_array($type, self::$_joinTypes)) { + throw new SelectException("Unrecognized method '$method()'"); + } + if (in_array($type, array(self::CROSS_JOIN, self::NATURAL_JOIN))) { + throw new SelectException("Cannot perform a joinUsing with method '$method()'"); + } + } else { + $type = self::INNER_JOIN; + } + array_unshift($args, $type); + return call_user_func_array(array($this, '_joinUsing'), $args); + } + + throw new SelectException("Unrecognized method '$method()'"); + } + + /** + * Implements magic method. + * + * @return string This object as a SELECT string. + */ + public function __toString() + { + try { + $sql = $this->assemble(); + } catch (DbException $e) { + trigger_error($e->getMessage(), E_USER_WARNING); + $sql = ''; + } + return (string)$sql; + } +} diff --git a/vendor/gipfl/zfdb/src/Statement.php b/vendor/gipfl/zfdb/src/Statement.php new file mode 100644 index 0000000..3288be6 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement.php @@ -0,0 +1,462 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb; + +use gipfl\ZfDb\Adapter\Adapter; +use gipfl\ZfDb\Profiler\ProfilerQuery; +use gipfl\ZfDb\Statement\Exception\StatementException; +use gipfl\ZfDb\Statement\StatementInterface; + +/** + * Abstract class to emulate a PDOStatement for native database adapters. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +abstract class Statement implements StatementInterface +{ + /** + * @var resource|object The driver level statement object/resource + */ + protected $_stmt = null; + + /** + * @var Adapter + */ + protected $_adapter = null; + + /** + * The current fetch mode. + * + * @var integer + */ + protected $_fetchMode = Db::FETCH_ASSOC; + + /** + * Attributes. + * + * @var array + */ + protected $_attribute = array(); + + /** + * Column result bindings. + * + * @var array + */ + protected $_bindColumn = array(); + + /** + * Query parameter bindings; covers bindParam() and bindValue(). + * + * @var array + */ + protected $_bindParam = array(); + + /** + * SQL string split into an array at placeholders. + * + * @var array + */ + protected $_sqlSplit = array(); + + /** + * Parameter placeholders in the SQL string by position in the split array. + * + * @var array + */ + protected $_sqlParam = array(); + + /** + * @var ProfilerQuery + */ + protected $_queryId = null; + + /** + * Constructor for a statement. + * + * @param Adapter $adapter + * @param mixed $sql Either a string or Zend_Db_Select. + */ + public function __construct($adapter, $sql) + { + $this->_adapter = $adapter; + if ($sql instanceof Select) { + $sql = $sql->assemble(); + } + $this->_parseParameters($sql); + $this->_prepare($sql); + + $this->_queryId = $this->_adapter->getProfiler()->queryStart($sql); + } + + /** + * Internal method called by abstract statment constructor to setup + * the driver level statement + * + * @return void + */ + protected function _prepare($sql) + { + return; + } + + /** + * @param string $sql + * @return void + */ + protected function _parseParameters($sql) + { + $sql = $this->_stripQuoted($sql); + + // split into text and params + $this->_sqlSplit = preg_split( + '/(\?|:[a-zA-Z0-9_]+)/', + $sql, + -1, + PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY + ); + + // map params + $this->_sqlParam = array(); + foreach ($this->_sqlSplit as $key => $val) { + if ($val == '?') { + if ($this->_adapter->supportsParameters('positional') === false) { + throw new StatementException("Invalid bind-variable position '$val'"); + } + } elseif ($val[0] == ':') { + if ($this->_adapter->supportsParameters('named') === false) { + throw new StatementException("Invalid bind-variable name '$val'"); + } + } + $this->_sqlParam[] = $val; + } + + // set up for binding + $this->_bindParam = array(); + } + + /** + * Remove parts of a SQL string that contain quoted strings + * of values or identifiers. + * + * @param string $sql + * @return string + */ + protected function _stripQuoted($sql) + { + // get the character for value quoting + // this should be ' + $q = $this->_adapter->quote('a'); + $q = $q[0]; + // get the value used as an escaped quote, + // e.g. \' or '' + $qe = $this->_adapter->quote($q); + $qe = substr($qe, 1, 2); + $qe = preg_quote($qe); + $escapeChar = substr($qe, 0, 1); + // remove 'foo\'bar' + if (!empty($q)) { + $escapeChar = preg_quote($escapeChar); + // this segfaults only after 65,000 characters instead of 9,000 + $sql = preg_replace("/$q([^$q{$escapeChar}]*|($qe)*)*$q/s", '', $sql); + } + + // get a version of the SQL statement with all quoted + // values and delimited identifiers stripped out + // remove "foo\"bar" + $sql = preg_replace("/\"(\\\\\"|[^\"])*\"/Us", '', $sql); + + // get the character for delimited id quotes, + // this is usually " but in MySQL is ` + $d = $this->_adapter->quoteIdentifier('a'); + $d = $d[0]; + // get the value used as an escaped delimited id quote, + // e.g. \" or "" or \` + $de = $this->_adapter->quoteIdentifier($d); + $de = substr($de, 1, 2); + $de = preg_quote($de); + // Note: $de and $d where never used..., now they are: + $sql = preg_replace("/$d($de|\\\\{2}|[^$d])*$d/Us", '', $sql); + return $sql; + } + + /** + * Bind a column of the statement result set to a PHP variable. + * + * @param string $column Name the column in the result set, either by + * position or by name. + * @param mixed $param Reference to the PHP variable containing the value. + * @param mixed $type OPTIONAL + * @return bool + */ + public function bindColumn($column, &$param, $type = null) + { + $this->_bindColumn[$column] =& $param; + return true; + } + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + */ + public function bindParam($parameter, &$variable, $type = null, $length = null, $options = null) + { + if (!is_int($parameter) && !is_string($parameter)) { + throw new StatementException('Invalid bind-variable position'); + } + + $position = null; + if (($intval = (int) $parameter) > 0 && $this->_adapter->supportsParameters('positional')) { + if ($intval >= 1 || $intval <= count($this->_sqlParam)) { + $position = $intval; + } + } elseif ($this->_adapter->supportsParameters('named')) { + if ($parameter[0] != ':') { + $parameter = ':' . $parameter; + } + if (in_array($parameter, $this->_sqlParam) !== false) { + $position = $parameter; + } + } + + if ($position === null) { + throw new StatementException("Invalid bind-variable position '$parameter'"); + } + + // Finally we are assured that $position is valid + $this->_bindParam[$position] =& $variable; + return $this->_bindParam($position, $variable, $type, $length, $options); + } + + /** + * Binds a value to a parameter. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $value Scalar value to bind to the parameter. + * @param mixed $type OPTIONAL Datatype of the parameter. + * @return bool + */ + public function bindValue($parameter, $value, $type = null) + { + return $this->bindParam($parameter, $value, $type); + } + + /** + * Executes a prepared statement. + * + * @param array $params OPTIONAL Values to bind to parameter placeholders. + * @return bool + */ + public function execute(array $params = null) + { + /* + * Simple case - no query profiler to manage. + */ + if ($this->_queryId === null) { + return $this->_execute($params); + } + + /* + * Do the same thing, but with query profiler + * management before and after the execute. + */ + $prof = $this->_adapter->getProfiler(); + $qp = $prof->getQueryProfile($this->_queryId); + if ($qp->hasEnded()) { + $this->_queryId = $prof->queryClone($qp); + $qp = $prof->getQueryProfile($this->_queryId); + } + if ($params !== null) { + $qp->bindParams($params); + } else { + $qp->bindParams($this->_bindParam); + } + $qp->start($this->_queryId); + + $retval = $this->_execute($params); + + $prof->queryEnd($this->_queryId); + + return $retval; + } + + /** + * Returns an array containing all of the result set rows. + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array Collection of rows, each in a format by the fetch mode. + */ + public function fetchAll($style = null, $col = null) + { + $data = array(); + if ($style === Db::FETCH_COLUMN && $col === null) { + $col = 0; + } + if ($col === null) { + while ($row = $this->fetch($style)) { + $data[] = $row; + } + } else { + while (false !== ($val = $this->fetchColumn($col))) { + $data[] = $val; + } + } + return $data; + } + + /** + * Returns a single column from the next row of a result set. + * + * @param int $col OPTIONAL Position of the column to fetch. + * @return string One value from the next row of result set, or false. + */ + public function fetchColumn($col = 0) + { + $data = array(); + $col = (int) $col; + $row = $this->fetch(Db::FETCH_NUM); + if (!is_array($row)) { + return false; + } + return $row[$col]; + } + + /** + * Fetches the next row and returns it as an object. + * + * @param string $class OPTIONAL Name of the class to create. + * @param array $config OPTIONAL Constructor arguments for the class. + * @return mixed One object instance of the specified class, or false. + */ + public function fetchObject($class = 'stdClass', array $config = array()) + { + $obj = new $class($config); + $row = $this->fetch(Db::FETCH_ASSOC); + if (!is_array($row)) { + return false; + } + foreach ($row as $key => $val) { + $obj->$key = $val; + } + return $obj; + } + + /** + * Retrieve a statement attribute. + * + * @param string $key Attribute name. + * @return mixed Attribute value. + */ + public function getAttribute($key) + { + if (array_key_exists($key, $this->_attribute)) { + return $this->_attribute[$key]; + } + + return null; + } + + /** + * Set a statement attribute. + * + * @param string $key Attribute name. + * @param mixed $val Attribute value. + */ + public function setAttribute($key, $val) + { + $this->_attribute[$key] = $val; + } + + /** + * Set the default fetch mode for this statement. + * + * @param int $mode The fetch mode. + * @return bool + * @throws StatementException + */ + public function setFetchMode($mode) + { + switch ($mode) { + case Db::FETCH_NUM: + case Db::FETCH_ASSOC: + case Db::FETCH_BOTH: + case Db::FETCH_OBJ: + $this->_fetchMode = $mode; + break; + case Db::FETCH_BOUND: + default: + $this->closeCursor(); + throw new StatementException('invalid fetch mode'); + } + + return true; + } + + /** + * Helper function to map retrieved row + * to bound column variables + * + * @param array $row + * @return bool True + */ + public function _fetchBound($row) + { + foreach ($row as $key => $value) { + // bindColumn() takes 1-based integer positions + // but fetch() returns 0-based integer indexes + if (is_int($key)) { + $key++; + } + // set results only to variables that were bound previously + if (isset($this->_bindColumn[$key])) { + $this->_bindColumn[$key] = $value; + } + } + return true; + } + + /** + * Gets the Zend_Db_Adapter_Abstract for this + * particular Zend_Db_Statement object. + * + * @return Adapter + */ + public function getAdapter() + { + return $this->_adapter; + } + + /** + * Gets the resource or object setup by the + * _parse + * @return mixed + */ + public function getDriverStatement() + { + return $this->_stmt; + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/Db2Statement.php b/vendor/gipfl/zfdb/src/Statement/Db2Statement.php new file mode 100644 index 0000000..ebee20f --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Db2Statement.php @@ -0,0 +1,334 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement; + +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Statement; +use gipfl\ZfDb\Statement\Exception\StatementExceptionDb2; + +/** + * Extends for DB2 native adapter. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Db2Statement extends Statement +{ + /** + * Column names. + */ + protected $_keys; + + /** + * Fetched result values. + */ + protected $_values; + + /** + * Prepare a statement handle. + * + * @param string $sql + * @return void + * @throws StatementExceptionDb2 + */ + public function _prepare($sql) + { + $connection = $this->_adapter->getConnection(); + + // db2_prepare on i5 emits errors, these need to be + // suppressed so that proper exceptions can be thrown + $this->_stmt = @db2_prepare($connection, $sql); + + if (!$this->_stmt) { + throw new StatementExceptionDb2( + db2_stmt_errormsg(), + db2_stmt_error() + ); + } + } + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + * @throws StatementExceptionDb2 + */ + public function _bindParam($parameter, &$variable, $type = null, $length = null, $options = null) + { + if ($type === null) { + $type = DB2_PARAM_IN; + } + + if (isset($options['data-type'])) { + $datatype = $options['data-type']; + } else { + $datatype = DB2_CHAR; + } + + if (!db2_bind_param($this->_stmt, $parameter, "variable", $type, $datatype)) { + throw new StatementExceptionDb2( + db2_stmt_errormsg(), + db2_stmt_error() + ); + } + + return true; + } + + /** + * Closes the cursor, allowing the statement to be executed again. + * + * @return bool + */ + public function closeCursor() + { + if (!$this->_stmt) { + return false; + } + db2_free_stmt($this->_stmt); + $this->_stmt = false; + return true; + } + + + /** + * Returns the number of columns in the result set. + * Returns null if the statement has no result set metadata. + * + * @return int The number of columns. + */ + public function columnCount() + { + if (!$this->_stmt) { + return false; + } + return db2_num_fields($this->_stmt); + } + + /** + * Retrieves the error code, if any, associated with the last operation on + * the statement handle. + * + * @return string error code. + */ + public function errorCode() + { + if (!$this->_stmt) { + return false; + } + + $error = db2_stmt_error(); + if ($error === '') { + return false; + } + + return $error; + } + + /** + * Retrieves an array of error information, if any, associated with the + * last operation on the statement handle. + * + * @return array|false + */ + public function errorInfo() + { + $error = $this->errorCode(); + if ($error === false) { + return false; + } + + /* + * Return three-valued array like PDO. But DB2 does not distinguish + * between SQLCODE and native RDBMS error code, so repeat the SQLCODE. + */ + return array( + $error, + $error, + db2_stmt_errormsg() + ); + } + + /** + * Executes a prepared statement. + * + * @param array $params OPTIONAL Values to bind to parameter placeholders. + * @return bool + * @throws StatementExceptionDb2 + */ + public function _execute(array $params = null) + { + if (!$this->_stmt) { + return false; + } + + $retval = true; + if ($params !== null) { + $retval = @db2_execute($this->_stmt, $params); + } else { + $retval = @db2_execute($this->_stmt); + } + + if ($retval === false) { + throw new StatementExceptionDb2( + db2_stmt_errormsg(), + db2_stmt_error() + ); + } + + $this->_keys = array(); + if ($field_num = $this->columnCount()) { + for ($i = 0; $i < $field_num; $i++) { + $name = db2_field_name($this->_stmt, $i); + $this->_keys[] = $name; + } + } + + $this->_values = array(); + if ($this->_keys) { + $this->_values = array_fill(0, count($this->_keys), null); + } + + return $retval; + } + + /** + * Fetches a row from the result set. + * + * @param int $style OPTIONAL Fetch mode for this fetch operation. + * @param int $cursor OPTIONAL Absolute, relative, or other. + * @param int $offset OPTIONAL Number for absolute or relative cursors. + * @return mixed Array, object, or scalar depending on fetch mode. + * @throws StatementExceptionDb2 + */ + public function fetch($style = null, $cursor = null, $offset = null) + { + if (!$this->_stmt) { + return false; + } + + if ($style === null) { + $style = $this->_fetchMode; + } + + switch ($style) { + case Db::FETCH_NUM: + $row = db2_fetch_array($this->_stmt); + break; + case Db::FETCH_ASSOC: + $row = db2_fetch_assoc($this->_stmt); + break; + case Db::FETCH_BOTH: + $row = db2_fetch_both($this->_stmt); + break; + case Db::FETCH_OBJ: + $row = db2_fetch_object($this->_stmt); + break; + case Db::FETCH_BOUND: + $row = db2_fetch_both($this->_stmt); + if ($row !== false) { + return $this->_fetchBound($row); + } + break; + default: + throw new StatementExceptionDb2("Invalid fetch mode '$style' specified"); + } + + return $row; + } + + /** + * Fetches the next row and returns it as an object. + * + * @param string $class OPTIONAL Name of the class to create. + * @param array $config OPTIONAL Constructor arguments for the class. + * @return mixed One object instance of the specified class. + */ + public function fetchObject($class = 'stdClass', array $config = array()) + { + $obj = $this->fetch(Db::FETCH_OBJ); + return $obj; + } + + /** + * Retrieves the next rowset (result set) for a SQL statement that has + * multiple result sets. An example is a stored procedure that returns + * the results of multiple queries. + * + * @return bool + * @throws StatementExceptionDb2 + */ + public function nextRowset() + { + throw new StatementExceptionDb2(__FUNCTION__ . '() is not implemented'); + } + + /** + * Returns the number of rows affected by the execution of the + * last INSERT, DELETE, or UPDATE statement executed by this + * statement object. + * + * @return int The number of rows affected. + */ + public function rowCount() + { + if (!$this->_stmt) { + return false; + } + + $num = @db2_num_rows($this->_stmt); + + if ($num === false) { + return 0; + } + + return $num; + } + + /** + * Returns an array containing all of the result set rows. + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array Collection of rows, each in a format by the fetch mode. + * + * Behaves like parent, but if limit() + * is used, the final result removes the extra column + * 'zend_db_rownum' + */ + public function fetchAll($style = null, $col = null) + { + $data = parent::fetchAll($style, $col); + $results = array(); + $remove = $this->_adapter->foldCase('ZEND_DB_ROWNUM'); + + foreach ($data as $row) { + if (is_array($row) && array_key_exists($remove, $row)) { + unset($row[$remove]); + } + $results[] = $row; + } + return $results; + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/Exception/StatementException.php b/vendor/gipfl/zfdb/src/Statement/Exception/StatementException.php new file mode 100644 index 0000000..48450fc --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Exception/StatementException.php @@ -0,0 +1,48 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement\Exception; + +use gipfl\ZfDb\Exception\DbException; + +/** + * Zend_Db_Statement_Exception + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class StatementException extends DbException +{ + /** + * Check if this general exception has a specific database driver specific exception nested inside. + * + * @return bool + */ + public function hasChainedException() + { + return ($this->getPrevious() !== null); + } + + /** + * @return \Exception|null + */ + public function getChainedException() + { + return $this->getPrevious(); + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionDb2.php b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionDb2.php new file mode 100644 index 0000000..23de2d7 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionDb2.php @@ -0,0 +1,50 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement\Exception; + +use gipfl\ZfDb\Statement\Exception\StatementException; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +class StatementExceptionDb2 extends StatementException +{ + /** + * @var string + */ + protected $code = '00000'; + + /** + * @var string + */ + protected $message = 'unknown exception'; + + /** + * @param string $msg + * @param string $state + */ + public function __construct($msg = 'unknown exception', $state = '00000') + { + $this->message = $msg; + $this->code = $state; + parent::__construct($this->message, $this->code); + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionMysqli.php b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionMysqli.php new file mode 100644 index 0000000..9adbbc3 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionMysqli.php @@ -0,0 +1,27 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement\Exception; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class StatementExceptionMysqli extends StatementException +{ +} diff --git a/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionOracle.php b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionOracle.php new file mode 100644 index 0000000..64e0de7 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionOracle.php @@ -0,0 +1,49 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement\Exception; + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +class StatementExceptionOracle extends StatementException +{ + protected $message = 'Unknown exception'; + protected $code = 0; + + public function __construct($error = null, $code = 0) + { + if (is_array($error)) { + if (!isset($error['offset'])) { + $this->message = $error['code']." ".$error['message']; + } else { + $this->message = $error['code']." ".$error['message']." "; + $this->message .= substr($error['sqltext'], 0, $error['offset']); + $this->message .= "*"; + $this->message .= substr($error['sqltext'], $error['offset']); + } + $this->code = $error['code']; + } + if (!$this->code && $code) { + $this->code = $code; + } + parent::__construct($this->message, $this->code); + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionSqlsrv.php b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionSqlsrv.php new file mode 100644 index 0000000..a2bcd16 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Exception/StatementExceptionSqlsrv.php @@ -0,0 +1,55 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement\Exception; + +/** + * @see StatementException + */ + +/** + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class StatementExceptionSqlsrv extends StatementException +{ + /** + * Constructor + * + * If $message is an array, the assumption is that the return value of + * sqlsrv_errors() was provided. If so, it then retrieves the most recent + * error from that stack, and sets the message and code based on it. + * + * @param null|array|string $message + * @param null|int $code + */ + public function __construct($message = null, $code = 0) + { + if (is_array($message)) { + // Error should be array of errors + // We only need first one (?) + if (isset($message[0])) { + $message = $message[0]; + } + + $code = (int) $message['code']; + $message = (string) $message['message']; + } + parent::__construct($message, $code); + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/MysqliStatement.php b/vendor/gipfl/zfdb/src/Statement/MysqliStatement.php new file mode 100644 index 0000000..4aa62d2 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/MysqliStatement.php @@ -0,0 +1,345 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement; + +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Statement; +use gipfl\ZfDb\Statement\Exception\StatementExceptionMysqli; + +/** + * Extends for Mysqli + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class MysqliStatement extends Statement +{ + /** + * Column names. + * + * @var array + */ + protected $_keys; + + /** + * Fetched result values. + * + * @var array + */ + protected $_values; + + /** + * @var array + */ + protected $_meta = null; + + /** + * @param string $sql + * @return void + * @throws StatementExceptionMysqli + */ + public function _prepare($sql) + { + $mysqli = $this->_adapter->getConnection(); + + $this->_stmt = $mysqli->prepare($sql); + + if ($this->_stmt === false || $mysqli->errno) { + /** + * @see StatementExceptionMysqli + */ + throw new StatementExceptionMysqli("Mysqli prepare error: " . $mysqli->error, $mysqli->errno); + } + } + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + * @throws StatementExceptionMysqli + */ + protected function _bindParam($parameter, &$variable, $type = null, $length = null, $options = null) + { + return true; + } + + /** + * Closes the cursor and the statement. + * + * @return bool + */ + public function close() + { + if ($this->_stmt) { + $r = $this->_stmt->close(); + $this->_stmt = null; + return $r; + } + return false; + } + + /** + * Closes the cursor, allowing the statement to be executed again. + * + * @return bool + */ + public function closeCursor() + { + if ($stmt = $this->_stmt) { + $mysqli = $this->_adapter->getConnection(); + while ($mysqli->more_results()) { + $mysqli->next_result(); + } + $this->_stmt->free_result(); + return $this->_stmt->reset(); + } + return false; + } + + /** + * Returns the number of columns in the result set. + * Returns null if the statement has no result set metadata. + * + * @return int The number of columns. + */ + public function columnCount() + { + if (isset($this->_meta) && $this->_meta) { + return $this->_meta->field_count; + } + return 0; + } + + /** + * Retrieves the error code, if any, associated with the last operation on + * the statement handle. + * + * @return string error code. + */ + public function errorCode() + { + if (!$this->_stmt) { + return false; + } + return substr($this->_stmt->sqlstate, 0, 5); + } + + /** + * Retrieves an array of error information, if any, associated with the + * last operation on the statement handle. + * + * @return array|bool + */ + public function errorInfo() + { + if (!$this->_stmt) { + return false; + } + return array( + substr($this->_stmt->sqlstate, 0, 5), + $this->_stmt->errno, + $this->_stmt->error, + ); + } + + /** + * Executes a prepared statement. + * + * @param array $params OPTIONAL Values to bind to parameter placeholders. + * @return bool + * @throws StatementExceptionMysqli + */ + public function _execute(array $params = null) + { + if (!$this->_stmt) { + return false; + } + + // if no params were given as an argument to execute(), + // then default to the _bindParam array + if ($params === null) { + $params = $this->_bindParam; + } + // send $params as input parameters to the statement + if ($params) { + array_unshift($params, str_repeat('s', count($params))); + $stmtParams = array(); + foreach ($params as $k => &$value) { + $stmtParams[$k] = &$value; + } + call_user_func_array( + array($this->_stmt, 'bind_param'), + $stmtParams + ); + } + + // execute the statement + $retval = $this->_stmt->execute(); + if ($retval === false) { + throw new StatementExceptionMysqli( + "Mysqli statement execute error : " . $this->_stmt->error, + $this->_stmt->errno + ); + } + + + // retain metadata + if ($this->_meta === null) { + $this->_meta = $this->_stmt->result_metadata(); + if ($this->_stmt->errno) { + /** + * @see StatementExceptionMysqli + */ + throw new StatementExceptionMysqli( + "Mysqli statement metadata error: " . $this->_stmt->error, + $this->_stmt->errno + ); + } + } + + // statements that have no result set do not return metadata + if ($this->_meta !== false) { + // get the column names that will result + $this->_keys = array(); + foreach ($this->_meta->fetch_fields() as $col) { + $this->_keys[] = $this->_adapter->foldCase($col->name); + } + + // set up a binding space for result variables + $this->_values = array_fill(0, count($this->_keys), null); + + // set up references to the result binding space. + // just passing $this->_values in the call_user_func_array() + // below won't work, you need references. + $refs = array(); + foreach ($this->_values as $i => &$f) { + $refs[$i] = &$f; + } + + $this->_stmt->store_result(); + // bind to the result variables + call_user_func_array( + array($this->_stmt, 'bind_result'), + $this->_values + ); + } + return $retval; + } + + + /** + * Fetches a row from the result set. + * + * @param int $style OPTIONAL Fetch mode for this fetch operation. + * @param int $cursor OPTIONAL Absolute, relative, or other. + * @param int $offset OPTIONAL Number for absolute or relative cursors. + * @return mixed Array, object, or scalar depending on fetch mode. + * @throws StatementExceptionMysqli + */ + public function fetch($style = null, $cursor = null, $offset = null) + { + if (!$this->_stmt) { + return false; + } + // fetch the next result + $retval = $this->_stmt->fetch(); + switch ($retval) { + case null: // end of data + case false: // error occurred + $this->_stmt->reset(); + return false; + default: + // fallthrough + } + + // make sure we have a fetch mode + if ($style === null) { + $style = $this->_fetchMode; + } + + // dereference the result values, otherwise things like fetchAll() + // return the same values for every entry (because of the reference). + $values = array(); + foreach ($this->_values as $key => $val) { + $values[] = $val; + } + + $row = false; + switch ($style) { + case Db::FETCH_NUM: + $row = $values; + break; + case Db::FETCH_ASSOC: + $row = array_combine($this->_keys, $values); + break; + case Db::FETCH_BOTH: + $assoc = array_combine($this->_keys, $values); + $row = array_merge($values, $assoc); + break; + case Db::FETCH_OBJ: + $row = (object) array_combine($this->_keys, $values); + break; + case Db::FETCH_BOUND: + $assoc = array_combine($this->_keys, $values); + $row = array_merge($values, $assoc); + return $this->_fetchBound($row); + default: + throw new StatementExceptionMysqli("Invalid fetch mode '$style' specified"); + } + + return $row; + } + + /** + * Retrieves the next rowset (result set) for a SQL statement that has + * multiple result sets. An example is a stored procedure that returns + * the results of multiple queries. + * + * @return bool + * @throws StatementExceptionMysqli + */ + public function nextRowset() + { + /** + * @see StatementExceptionMysqli + */ + throw new StatementExceptionMysqli(__FUNCTION__.'() is not implemented'); + } + + /** + * Returns the number of rows affected by the execution of the + * last INSERT, DELETE, or UPDATE statement executed by this + * statement object. + * + * @return int The number of rows affected. + */ + public function rowCount() + { + if (!$this->_adapter) { + return false; + } + $mysqli = $this->_adapter->getConnection(); + return $mysqli->affected_rows; + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/OracleStatement.php b/vendor/gipfl/zfdb/src/Statement/OracleStatement.php new file mode 100644 index 0000000..532b227 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/OracleStatement.php @@ -0,0 +1,519 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement; + +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Statement; +use gipfl\ZfDb\Statement\Exception\StatementException; +use gipfl\ZfDb\Statement\Exception\StatementExceptionOracle; + +/** + * Extends for Oracle. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class OracleStatement extends Statement +{ + /** + * Column names. + */ + protected $_keys; + + /** + * Fetched result values. + */ + protected $_values; + + /** + * Check if LOB field are returned as string + * instead of OCI-Lob object + * + * @var boolean + */ + protected $_lobAsString = false; + + /** + * Activate/deactivate return of LOB as string + * + * @param string $lob_as_string + * @return OracleStatement + */ + public function setLobAsString($lob_as_string) + { + $this->_lobAsString = (bool) $lob_as_string; + return $this; + } + + /** + * Return whether or not LOB are returned as string + * + * @return boolean + */ + public function getLobAsString() + { + return $this->_lobAsString; + } + + /** + * Prepares statement handle + * + * @param string $sql + * @return void + * @throws StatementExceptionOracle + */ + protected function _prepare($sql) + { + $connection = $this->_adapter->getConnection(); + $this->_stmt = @oci_parse($connection, $sql); + if (!$this->_stmt) { + throw new StatementExceptionOracle(oci_error($connection)); + } + } + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + * @throws StatementException + */ + protected function _bindParam($parameter, &$variable, $type = null, $length = null, $options = null) + { + // default value + if ($type === null) { + $type = SQLT_CHR; + } + + // default value + if ($length === null) { + $length = -1; + } + + $retval = @oci_bind_by_name($this->_stmt, $parameter, $variable, $length, $type); + if ($retval === false) { + throw new StatementExceptionOracle(oci_error($this->_stmt)); + } + + return true; + } + + /** + * Closes the cursor, allowing the statement to be executed again. + * + * @return bool + */ + public function closeCursor() + { + if (!$this->_stmt) { + return false; + } + + oci_free_statement($this->_stmt); + $this->_stmt = false; + return true; + } + + /** + * Returns the number of columns in the result set. + * Returns null if the statement has no result set metadata. + * + * @return int The number of columns. + */ + public function columnCount() + { + if (!$this->_stmt) { + return false; + } + + return oci_num_fields($this->_stmt); + } + + + /** + * Retrieves the error code, if any, associated with the last operation on + * the statement handle. + * + * @return string error code. + */ + public function errorCode() + { + if (!$this->_stmt) { + return false; + } + + $error = oci_error($this->_stmt); + + if (!$error) { + return false; + } + + return $error['code']; + } + + + /** + * Retrieves an array of error information, if any, associated with the + * last operation on the statement handle. + * + * @return array|bool + */ + public function errorInfo() + { + if (!$this->_stmt) { + return false; + } + + $error = oci_error($this->_stmt); + if (!$error) { + return false; + } + + if (isset($error['sqltext'])) { + return array( + $error['code'], + $error['message'], + $error['offset'], + $error['sqltext'], + ); + } else { + return array( + $error['code'], + $error['message'], + ); + } + } + + + /** + * Executes a prepared statement. + * + * @param array $params OPTIONAL Values to bind to parameter placeholders. + * @return bool + * @throws StatementException + */ + public function _execute(array $params = null) + { + $connection = $this->_adapter->getConnection(); + + if (!$this->_stmt) { + return false; + } + + if ($params !== null) { + if (!is_array($params)) { + $params = array($params); + } + $error = false; + foreach (array_keys($params) as $name) { + if (!$this->bindParam($name, $params[$name], null, -1)) { + $error = true; + break; + } + } + if ($error) { + throw new StatementExceptionOracle(oci_error($this->_stmt)); + } + } + + $retval = @oci_execute($this->_stmt, $this->_adapter->_getExecuteMode()); + if ($retval === false) { + throw new StatementExceptionOracle(oci_error($this->_stmt)); + } + + $this->_keys = array(); + if ($field_num = oci_num_fields($this->_stmt)) { + for ($i = 1; $i <= $field_num; $i++) { + $name = oci_field_name($this->_stmt, $i); + $this->_keys[] = $name; + } + } + + $this->_values = array(); + if ($this->_keys) { + $this->_values = array_fill(0, count($this->_keys), null); + } + + return $retval; + } + + /** + * Fetches a row from the result set. + * + * @param int $style OPTIONAL Fetch mode for this fetch operation. + * @param int $cursor OPTIONAL Absolute, relative, or other. + * @param int $offset OPTIONAL Number for absolute or relative cursors. + * @return mixed array, object, or scalar depending on fetch mode. + * @throws StatementException + */ + public function fetch($style = null, $cursor = null, $offset = null) + { + if (!$this->_stmt) { + return false; + } + + if ($style === null) { + $style = $this->_fetchMode; + } + + $lob_as_string = $this->getLobAsString() ? OCI_RETURN_LOBS : 0; + + switch ($style) { + case Db::FETCH_NUM: + $row = oci_fetch_array($this->_stmt, OCI_NUM | OCI_RETURN_NULLS | $lob_as_string); + break; + case Db::FETCH_ASSOC: + $row = oci_fetch_array($this->_stmt, OCI_ASSOC | OCI_RETURN_NULLS | $lob_as_string); + break; + case Db::FETCH_BOTH: + $row = oci_fetch_array($this->_stmt, OCI_BOTH | OCI_RETURN_NULLS | $lob_as_string); + break; + case Db::FETCH_OBJ: + $row = oci_fetch_object($this->_stmt); + break; + case Db::FETCH_BOUND: + $row = oci_fetch_array($this->_stmt, OCI_BOTH | OCI_RETURN_NULLS | $lob_as_string); + if ($row !== false) { + return $this->_fetchBound($row); + } + break; + default: + /** + * @see StatementExceptionOracle + */ + throw new StatementExceptionOracle( + array( + 'code' => 'HYC00', + 'message' => "Invalid fetch mode '$style' specified" + ) + ); + break; + } + + if (! $row && $error = oci_error($this->_stmt)) { + throw new StatementExceptionOracle($error); + } + + if (is_array($row) && array_key_exists('zend_db_rownum', $row)) { + unset($row['zend_db_rownum']); + } + + return $row; + } + + /** + * Returns an array containing all of the result set rows. + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array|false Collection of rows, each in a format by the fetch mode. + * @throws StatementException + */ + public function fetchAll($style = null, $col = 0) + { + if (!$this->_stmt) { + return false; + } + + // make sure we have a fetch mode + if ($style === null) { + $style = $this->_fetchMode; + } + + $flags = OCI_FETCHSTATEMENT_BY_ROW; + + switch ($style) { + case Db::FETCH_BOTH: + /** + * @see StatementExceptionOracle + */ + throw new StatementExceptionOracle( + array( + 'code' => 'HYC00', + 'message' => "OCI8 driver does not support fetchAll(FETCH_BOTH), use fetch() in a loop instead" + ) + ); + // notreached + $flags |= OCI_NUM; + $flags |= OCI_ASSOC; + break; + case Db::FETCH_NUM: + $flags |= OCI_NUM; + break; + case Db::FETCH_ASSOC: + $flags |= OCI_ASSOC; + break; + case Db::FETCH_OBJ: + break; + case Db::FETCH_COLUMN: + $flags = $flags &~ OCI_FETCHSTATEMENT_BY_ROW; + $flags |= OCI_FETCHSTATEMENT_BY_COLUMN; + $flags |= OCI_NUM; + break; + default: + throw new StatementExceptionOracle( + array( + 'code' => 'HYC00', + 'message' => "Invalid fetch mode '$style' specified" + ) + ); + break; + } + + $result = array(); + if ($flags != OCI_FETCHSTATEMENT_BY_ROW) { /* not Zend_Db::FETCH_OBJ */ + if (! ($rows = oci_fetch_all($this->_stmt, $result, 0, -1, $flags) )) { + if ($error = oci_error($this->_stmt)) { + throw new StatementExceptionOracle($error); + } + if (!$rows) { + return array(); + } + } + if ($style == Db::FETCH_COLUMN) { + $result = $result[$col]; + } + foreach ($result as &$row) { + if (is_array($row) && array_key_exists('zend_db_rownum', $row)) { + unset($row['zend_db_rownum']); + } + } + } else { + while (($row = oci_fetch_object($this->_stmt)) !== false) { + $result [] = $row; + } + if ($error = oci_error($this->_stmt)) { + throw new StatementExceptionOracle($error); + } + } + + return $result; + } + + + /** + * Returns a single column from the next row of a result set. + * + * @param int $col OPTIONAL Position of the column to fetch. + * @return string + * @throws StatementException + */ + public function fetchColumn($col = 0) + { + if (!$this->_stmt) { + return false; + } + + if (!oci_fetch($this->_stmt)) { + // if no error, there is simply no record + if (!$error = oci_error($this->_stmt)) { + return false; + } + throw new StatementExceptionOracle($error); + } + + $data = oci_result($this->_stmt, $col+1); //1-based + if ($data === false) { + throw new StatementExceptionOracle(oci_error($this->_stmt)); + } + + if ($this->getLobAsString()) { + // instanceof doesn't allow '-', we must use a temporary string + $type = 'OCI-Lob'; + if ($data instanceof $type) { + $data = $data->read($data->size()); + } + } + + return $data; + } + + /** + * Fetches the next row and returns it as an object. + * + * @param string $class OPTIONAL Name of the class to create. + * @param array $config OPTIONAL Constructor arguments for the class. + * @return mixed One object instance of the specified class. + * @throws StatementException + */ + public function fetchObject($class = 'stdClass', array $config = array()) + { + if (!$this->_stmt) { + return false; + } + + $obj = oci_fetch_object($this->_stmt); + + if ($error = oci_error($this->_stmt)) { + throw new StatementExceptionOracle($error); + } + + /* @todo XXX handle parameters */ + + return $obj; + } + + /** + * Retrieves the next rowset (result set) for a SQL statement that has + * multiple result sets. An example is a stored procedure that returns + * the results of multiple queries. + * + * @return bool + * @throws StatementException + */ + public function nextRowset() + { + /** + * @see StatementExceptionOracle + */ + throw new StatementExceptionOracle( + array( + 'code' => 'HYC00', + 'message' => 'Optional feature not implemented' + ) + ); + } + + /** + * Returns the number of rows affected by the execution of the + * last INSERT, DELETE, or UPDATE statement executed by this + * statement object. + * + * @return int The number of rows affected. + * @throws StatementException + */ + public function rowCount() + { + if (!$this->_stmt) { + return false; + } + + $num_rows = oci_num_rows($this->_stmt); + + if ($num_rows === false) { + throw new StatementExceptionOracle(oci_error($this->_stmt)); + } + + return $num_rows; + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/Pdo/IbmPdoStatement.php b/vendor/gipfl/zfdb/src/Statement/Pdo/IbmPdoStatement.php new file mode 100644 index 0000000..6e6d43a --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Pdo/IbmPdoStatement.php @@ -0,0 +1,86 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement\Pdo; + +use gipfl\ZfDb\Statement\PdoStatement; +use gipfl\ZfDb\Statement\Exception\StatementException; +use PDOException; + +/** + * Proxy class to wrap a PDOStatement object for IBM Databases. + * Matches the interface of PDOStatement. All methods simply proxy to the + * matching method in PDOStatement. PDOExceptions thrown by PDOStatement + * are re-thrown as Zend_Db_Statement_Exception. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class IbmPdoStatement extends PdoStatement +{ + /** + * Returns an array containing all of the result set rows. + * + * Behaves like parent, but if limit() + * is used, the final result removes the extra column + * 'zend_db_rownum' + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array Collection of rows, each in a format by the fetch mode. + * @throws StatementException + */ + public function fetchAll($style = null, $col = null) + { + $data = parent::fetchAll($style, $col); + $results = array(); + $remove = $this->_adapter->foldCase('ZEND_DB_ROWNUM'); + + foreach ($data as $row) { + if (is_array($row) && array_key_exists($remove, $row)) { + unset($row[$remove]); + } + $results[] = $row; + } + return $results; + } + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + * @throws StatementException + */ + public function _bindParam($parameter, &$variable, $type = null, $length = null, $options = null) + { + try { + if (($type === null) && ($length === null) && ($options === null)) { + return $this->_stmt->bindParam($parameter, $variable); + } else { + return $this->_stmt->bindParam($parameter, $variable, $type, $length, $options); + } + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/Pdo/OciPdoStatement.php b/vendor/gipfl/zfdb/src/Statement/Pdo/OciPdoStatement.php new file mode 100644 index 0000000..ed9b98c --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/Pdo/OciPdoStatement.php @@ -0,0 +1,83 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement\Pdo; + +use gipfl\ZfDb\Statement\PdoStatement; +use gipfl\ZfDb\Statement\Exception\StatementException; + +/** + * Proxy class to wrap a PDOStatement object for IBM Databases. + * Matches the interface of PDOStatement. All methods simply proxy to the + * matching method in PDOStatement. PDOExceptions thrown by PDOStatement + * are re-thrown as Zend_Db_Statement_Exception. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class OciPdoStatement extends PdoStatement +{ + /** + * Returns an array containing all of the result set rows. + * + * Behaves like parent, but if limit() + * is used, the final result removes the extra column + * 'zend_db_rownum' + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array Collection of rows, each in a format by the fetch mode. + * @throws StatementException + */ + public function fetchAll($style = null, $col = null) + { + $data = parent::fetchAll($style, $col); + $results = array(); + $remove = $this->_adapter->foldCase('zend_db_rownum'); + + foreach ($data as $row) { + if (is_array($row) && array_key_exists($remove, $row)) { + unset($row[$remove]); + } + $results[] = $row; + } + + return $results; + } + + /** + * Fetches a row from the result set. + * + * @param int $style OPTIONAL Fetch mode for this fetch operation. + * @param int $cursor OPTIONAL Absolute, relative, or other. + * @param int $offset OPTIONAL Number for absolute or relative cursors. + * @return mixed Array, object, or scalar depending on fetch mode. + * @throws StatementException + */ + public function fetch($style = null, $cursor = null, $offset = null) + { + $row = parent::fetch($style, $cursor, $offset); + + $remove = $this->_adapter->foldCase('zend_db_rownum'); + if (is_array($row) && array_key_exists($remove, $row)) { + unset($row[$remove]); + } + + return $row; + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/PdoStatement.php b/vendor/gipfl/zfdb/src/Statement/PdoStatement.php new file mode 100644 index 0000000..ac49eb6 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/PdoStatement.php @@ -0,0 +1,417 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement; + +use gipfl\ZfDb\Statement; +use gipfl\ZfDb\Statement\Exception\StatementException; +use IteratorAggregate; +use IteratorIterator; +use PDO; +use PDOException; + +/** + * Proxy class to wrap a PDOStatement object. + * Matches the interface of PDOStatement. All methods simply proxy to the + * matching method in PDOStatement. PDOExceptions thrown by PDOStatement + * are re-thrown as Zend_Db_Statement_Exception. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class PdoStatement extends Statement implements IteratorAggregate +{ + /** + * @var int + */ + protected $_fetchMode = PDO::FETCH_ASSOC; + + /** + * Prepare a string SQL statement and create a statement object. + * + * @param string $sql + * @return void + * @throws StatementException + */ + protected function _prepare($sql) + { + try { + $this->_stmt = $this->_adapter->getConnection()->prepare($sql); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Bind a column of the statement result set to a PHP variable. + * + * @param string $column Name the column in the result set, either by + * position or by name. + * @param mixed $param Reference to the PHP variable containing the value. + * @param mixed $type OPTIONAL + * @return bool + * @throws StatementException + */ + public function bindColumn($column, &$param, $type = null) + { + try { + if ($type === null) { + return $this->_stmt->bindColumn($column, $param); + } else { + return $this->_stmt->bindColumn($column, $param, $type); + } + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + * @throws StatementException + */ + protected function _bindParam($parameter, &$variable, $type = null, $length = null, $options = null) + { + try { + if ($type === null) { + if (is_bool($variable)) { + $type = PDO::PARAM_BOOL; + } elseif ($variable === null) { + $type = PDO::PARAM_NULL; + } elseif (is_integer($variable)) { + $type = PDO::PARAM_INT; + } else { + $type = PDO::PARAM_STR; + } + } + return $this->_stmt->bindParam($parameter, $variable, $type, $length, $options); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Binds a value to a parameter. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $value Scalar value to bind to the parameter. + * @param mixed $type OPTIONAL Datatype of the parameter. + * @return bool + * @throws StatementException + */ + public function bindValue($parameter, $value, $type = null) + { + if (is_string($parameter) && $parameter[0] != ':') { + $parameter = ":$parameter"; + } + + $this->_bindParam[$parameter] = $value; + + try { + if ($type === null) { + return $this->_stmt->bindValue($parameter, $value); + } else { + return $this->_stmt->bindValue($parameter, $value, $type); + } + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Closes the cursor, allowing the statement to be executed again. + * + * @return bool + * @throws StatementException + */ + public function closeCursor() + { + try { + return $this->_stmt->closeCursor(); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns the number of columns in the result set. + * Returns null if the statement has no result set metadata. + * + * @return int The number of columns. + * @throws StatementException + */ + public function columnCount() + { + try { + return $this->_stmt->columnCount(); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Retrieves the error code, if any, associated with the last operation on + * the statement handle. + * + * @return string error code. + * @throws StatementException + */ + public function errorCode() + { + try { + return $this->_stmt->errorCode(); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Retrieves an array of error information, if any, associated with the + * last operation on the statement handle. + * + * @return array + * @throws StatementException + */ + public function errorInfo() + { + try { + return $this->_stmt->errorInfo(); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Executes a prepared statement. + * + * @param array $params OPTIONAL Values to bind to parameter placeholders. + * @return bool + * @throws StatementException + */ + public function _execute(array $params = null) + { + try { + if ($params !== null) { + return $this->_stmt->execute($params); + } else { + return $this->_stmt->execute(); + } + } catch (PDOException $e) { + $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); + throw new StatementException($message, (int) $e->getCode(), $e); + } + } + + /** + * Fetches a row from the result set. + * + * @param int $style OPTIONAL Fetch mode for this fetch operation. + * @param int $cursor OPTIONAL Absolute, relative, or other. + * @param int $offset OPTIONAL Number for absolute or relative cursors. + * @return mixed Array, object, or scalar depending on fetch mode. + * @throws StatementException + */ + public function fetch($style = null, $cursor = PDO::FETCH_ORI_NEXT, $offset = 0) + { + if ($style === null) { + $style = $this->_fetchMode; + } + try { + return $this->_stmt->fetch($style, $cursor, $offset); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Required by IteratorAggregate interface + * + * @return IteratorIterator + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new IteratorIterator($this->_stmt); + } + + /** + * Returns an array containing all of the result set rows. + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array Collection of rows, each in a format by the fetch mode. + * @throws StatementException + */ + public function fetchAll($style = null, $col = null) + { + if ($style === null) { + $style = $this->_fetchMode; + } + try { + if ($style == PDO::FETCH_COLUMN) { + if ($col === null) { + $col = 0; + } + return $this->_stmt->fetchAll($style, $col); + } else { + return $this->_stmt->fetchAll($style); + } + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns a single column from the next row of a result set. + * + * @param int $col OPTIONAL Position of the column to fetch. + * @return string + * @throws StatementException + */ + public function fetchColumn($col = 0) + { + try { + return $this->_stmt->fetchColumn($col); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Fetches the next row and returns it as an object. + * + * @param string $class OPTIONAL Name of the class to create. + * @param array $config OPTIONAL Constructor arguments for the class. + * @return mixed One object instance of the specified class. + * @throws StatementException + */ + public function fetchObject($class = 'stdClass', array $config = array()) + { + try { + return $this->_stmt->fetchObject($class, $config); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Retrieve a statement attribute. + * + * @param integer $key Attribute name. + * @return mixed Attribute value. + * @throws StatementException + */ + public function getAttribute($key) + { + try { + return $this->_stmt->getAttribute($key); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns metadata for a column in a result set. + * + * @param int $column + * @return mixed + * @throws StatementException + */ + public function getColumnMeta($column) + { + try { + return $this->_stmt->getColumnMeta($column); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Retrieves the next rowset (result set) for a SQL statement that has + * multiple result sets. An example is a stored procedure that returns + * the results of multiple queries. + * + * @return bool + * @throws StatementException + */ + public function nextRowset() + { + try { + return $this->_stmt->nextRowset(); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns the number of rows affected by the execution of the + * last INSERT, DELETE, or UPDATE statement executed by this + * statement object. + * + * @return int The number of rows affected. + * @throws StatementException + */ + public function rowCount() + { + try { + return $this->_stmt->rowCount(); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Set a statement attribute. + * + * @param string $key Attribute name. + * @param mixed $val Attribute value. + * @return bool + * @throws StatementException + */ + public function setAttribute($key, $val) + { + try { + return $this->_stmt->setAttribute($key, $val); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Set the default fetch mode for this statement. + * + * @param int $mode The fetch mode. + * @return bool + * @throws StatementException + */ + public function setFetchMode($mode) + { + $this->_fetchMode = $mode; + try { + return $this->_stmt->setFetchMode($mode); + } catch (PDOException $e) { + throw new StatementException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/SqlsrvStatement.php b/vendor/gipfl/zfdb/src/Statement/SqlsrvStatement.php new file mode 100644 index 0000000..d5fe23a --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/SqlsrvStatement.php @@ -0,0 +1,423 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement; + +use gipfl\ZfDb\Db; +use gipfl\ZfDb\Statement\Exception\StatementException; +use gipfl\ZfDb\Statement\Exception\StatementExceptionSqlsrv; +use gipfl\ZfDb\Statement; + +/** + * Extends for Microsoft SQL Server Driver for PHP + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class SqlsrvStatement extends Statement +{ + /** + * The connection_stmt object original string. + */ + protected $_originalSQL; + + /** + * Column names. + */ + protected $_keys; + + /** + * Query executed + */ + protected $_executed = false; + + /** + * Prepares statement handle + * + * @param string $sql + * @return void + * @throws StatementExceptionSqlsrv + */ + protected function _prepare($sql) + { + $connection = $this->_adapter->getConnection(); + + $this->_stmt = sqlsrv_prepare($connection, $sql); + + if (!$this->_stmt) { + throw new StatementExceptionSqlsrv(sqlsrv_errors()); + } + + $this->_originalSQL = $sql; + } + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + * @throws StatementException + */ + protected function _bindParam($parameter, &$variable, $type = null, $length = null, $options = null) + { + //Sql server doesn't support bind by name + return true; + } + + /** + * Closes the cursor, allowing the statement to be executed again. + * + * @return bool + */ + public function closeCursor() + { + if (!$this->_stmt) { + return false; + } + + sqlsrv_free_stmt($this->_stmt); + $this->_stmt = false; + return true; + } + + /** + * Returns the number of columns in the result set. + * Returns null if the statement has no result set metadata. + * + * @return int The number of columns. + */ + public function columnCount() + { + if ($this->_stmt && $this->_executed) { + return sqlsrv_num_fields($this->_stmt); + } + + return 0; + } + + + /** + * Retrieves the error code, if any, associated with the last operation on + * the statement handle. + * + * @return string error code. + */ + public function errorCode() + { + if (!$this->_stmt) { + return false; + } + + $error = sqlsrv_errors(); + if (!$error) { + return false; + } + + return $error[0]['code']; + } + + /** + * Retrieves an array of error information, if any, associated with the + * last operation on the statement handle. + * + * @return array|bool + */ + public function errorInfo() + { + if (!$this->_stmt) { + return false; + } + + $error = sqlsrv_errors(); + if (!$error) { + return false; + } + + return array( + $error[0]['code'], + $error[0]['message'], + ); + } + + + /** + * Executes a prepared statement. + * + * @param array $params OPTIONAL Values to bind to parameter placeholders. + * @return bool + * @throws StatementException + */ + public function _execute(array $params = null) + { + $connection = $this->_adapter->getConnection(); + if (!$this->_stmt) { + return false; + } + + if ($params !== null) { + if (!is_array($params)) { + $params = array($params); + } + $error = false; + + // make all params passed by reference + $params_ = array(); + $temp = array(); + $i = 1; + foreach ($params as $param) { + $temp[$i] = $param; + $params_[] = &$temp[$i]; + $i++; + } + $params = $params_; + } + + $this->_stmt = sqlsrv_query($connection, $this->_originalSQL, $params); + + if (!$this->_stmt) { + throw new StatementExceptionSqlsrv(sqlsrv_errors()); + } + + $this->_executed = true; + + return (!$this->_stmt); + } + + /** + * Fetches a row from the result set. + * + * @param int $style OPTIONAL Fetch mode for this fetch operation. + * @param int $cursor OPTIONAL Absolute, relative, or other. + * @param int $offset OPTIONAL Number for absolute or relative cursors. + * @return mixed Array, object, or scalar depending on fetch mode. + * @throws StatementException + */ + public function fetch($style = null, $cursor = null, $offset = null) + { + if (!$this->_stmt) { + return false; + } + + if (null === $style) { + $style = $this->_fetchMode; + } + + $values = sqlsrv_fetch_array($this->_stmt, SQLSRV_FETCH_ASSOC); + + if (!$values && (null !== $error = sqlsrv_errors())) { + throw new StatementExceptionSqlsrv($error); + } + + if (null === $values) { + return null; + } + + if (!$this->_keys) { + foreach ($values as $key => $value) { + $this->_keys[] = $this->_adapter->foldCase($key); + } + } + + $values = array_values($values); + + $row = false; + switch ($style) { + case Db::FETCH_NUM: + $row = $values; + break; + case Db::FETCH_ASSOC: + $row = array_combine($this->_keys, $values); + break; + case Db::FETCH_BOTH: + $assoc = array_combine($this->_keys, $values); + $row = array_merge($values, $assoc); + break; + case Db::FETCH_OBJ: + $row = (object) array_combine($this->_keys, $values); + break; + case Db::FETCH_BOUND: + $assoc = array_combine($this->_keys, $values); + $row = array_merge($values, $assoc); + $row = $this->_fetchBound($row); + break; + default: + throw new StatementExceptionSqlsrv("Invalid fetch mode '$style' specified"); + } + + return $row; + } + + /** + * Returns a single column from the next row of a result set. + * + * @param int $col OPTIONAL Position of the column to fetch. + * @return string + * @throws StatementException + */ + public function fetchColumn($col = 0) + { + if (!$this->_stmt) { + return false; + } + + if (!sqlsrv_fetch($this->_stmt)) { + if (null !== $error = sqlsrv_errors()) { + throw new StatementExceptionSqlsrv($error); + } + + // If no error, there is simply no record + return false; + } + + $data = sqlsrv_get_field($this->_stmt, $col); //0-based + if ($data === false) { + throw new StatementExceptionSqlsrv(sqlsrv_errors()); + } + + return $data; + } + + /** + * Fetches the next row and returns it as an object. + * + * @param string $class OPTIONAL Name of the class to create. + * @param array $config OPTIONAL Constructor arguments for the class. + * @return mixed One object instance of the specified class. + * @throws StatementException + */ + public function fetchObject($class = 'stdClass', array $config = array()) + { + if (!$this->_stmt) { + return false; + } + + $obj = sqlsrv_fetch_object($this->_stmt); + + if ($error = sqlsrv_errors()) { + throw new StatementExceptionSqlsrv($error); + } + + /* @todo XXX handle parameters */ + + if (null === $obj) { + return false; + } + + return $obj; + } + + /** + * Returns metadata for a column in a result set. + * + * @param int $column + * @return mixed + * @throws StatementExceptionSqlsrv + */ + public function getColumnMeta($column) + { + $fields = sqlsrv_field_metadata($this->_stmt); + + if (!$fields) { + throw new StatementExceptionSqlsrv('Column metadata can not be fetched'); + } + + if (!isset($fields[$column])) { + throw new StatementExceptionSqlsrv('Column index does not exist in statement'); + } + + return $fields[$column]; + } + + /** + * Retrieves the next rowset (result set) for a SQL statement that has + * multiple result sets. An example is a stored procedure that returns + * the results of multiple queries. + * + * @return bool + * @throws StatementException + */ + public function nextRowset() + { + if (sqlsrv_next_result($this->_stmt) === false) { + throw new StatementExceptionSqlsrv(sqlsrv_errors()); + } + + // reset column keys + $this->_keys = null; + + return true; + } + + /** + * Returns the number of rows affected by the execution of the + * last INSERT, DELETE, or UPDATE statement executed by this + * statement object. + * + * @return int The number of rows affected. + * @throws StatementException + */ + public function rowCount() + { + if (!$this->_stmt) { + return false; + } + + if (!$this->_executed) { + return 0; + } + + $num_rows = sqlsrv_rows_affected($this->_stmt); + + // Strict check is necessary; 0 is a valid return value + if ($num_rows === false) { + throw new StatementExceptionSqlsrv(sqlsrv_errors()); + } + + return $num_rows; + } + + /** + * Returns an array containing all of the result set rows. + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array Collection of rows, each in a format by the fetch mode. + * + * Behaves like parent, but if limit() + * is used, the final result removes the extra column + * 'zend_db_rownum' + */ + public function fetchAll($style = null, $col = null) + { + $data = parent::fetchAll($style, $col); + $results = array(); + $remove = $this->_adapter->foldCase('ZEND_DB_ROWNUM'); + + foreach ($data as $row) { + if (is_array($row) && array_key_exists($remove, $row)) { + unset($row[$remove]); + } + $results[] = $row; + } + return $results; + } +} diff --git a/vendor/gipfl/zfdb/src/Statement/StatementInterface.php b/vendor/gipfl/zfdb/src/Statement/StatementInterface.php new file mode 100644 index 0000000..3b2a0b4 --- /dev/null +++ b/vendor/gipfl/zfdb/src/Statement/StatementInterface.php @@ -0,0 +1,198 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id$ + */ +namespace gipfl\ZfDb\Statement; + +use gipfl\ZfDb\Statement\Exception\StatementException; + +/** + * Emulates a PDOStatement for native database adapters. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +interface StatementInterface +{ + /** + * Bind a column of the statement result set to a PHP variable. + * + * @param string $column Name the column in the result set, either by + * position or by name. + * @param mixed $param Reference to the PHP variable containing the value. + * @param mixed $type OPTIONAL + * @return bool + * @throws StatementException + */ + public function bindColumn($column, &$param, $type = null); + + /** + * Binds a parameter to the specified variable name. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $variable Reference to PHP variable containing the value. + * @param mixed $type OPTIONAL Datatype of SQL parameter. + * @param mixed $length OPTIONAL Length of SQL parameter. + * @param mixed $options OPTIONAL Other options. + * @return bool + * @throws StatementException + */ + public function bindParam($parameter, &$variable, $type = null, $length = null, $options = null); + + /** + * Binds a value to a parameter. + * + * @param mixed $parameter Name the parameter, either integer or string. + * @param mixed $value Scalar value to bind to the parameter. + * @param mixed $type OPTIONAL Datatype of the parameter. + * @return bool + * @throws StatementException + */ + public function bindValue($parameter, $value, $type = null); + + /** + * Closes the cursor, allowing the statement to be executed again. + * + * @return bool + * @throws StatementException + */ + public function closeCursor(); + + /** + * Returns the number of columns in the result set. + * Returns null if the statement has no result set metadata. + * + * @return int The number of columns. + * @throws StatementException + */ + public function columnCount(); + + /** + * Retrieves the error code, if any, associated with the last operation on + * the statement handle. + * + * @return string error code. + * @throws StatementException + */ + public function errorCode(); + + /** + * Retrieves an array of error information, if any, associated with the + * last operation on the statement handle. + * + * @return array + * @throws StatementException + */ + public function errorInfo(); + + /** + * Executes a prepared statement. + * + * @param array $params OPTIONAL Values to bind to parameter placeholders. + * @return bool + * @throws StatementException + */ + public function execute(array $params = array()); + + /** + * Fetches a row from the result set. + * + * @param int $style OPTIONAL Fetch mode for this fetch operation. + * @param int $cursor OPTIONAL Absolute, relative, or other. + * @param int $offset OPTIONAL Number for absolute or relative cursors. + * @return mixed Array, object, or scalar depending on fetch mode. + * @throws StatementException + */ + public function fetch($style = null, $cursor = null, $offset = null); + + /** + * Returns an array containing all of the result set rows. + * + * @param int $style OPTIONAL Fetch mode. + * @param int $col OPTIONAL Column number, if fetch mode is by column. + * @return array Collection of rows, each in a format by the fetch mode. + * @throws StatementException + */ + public function fetchAll($style = null, $col = null); + + /** + * Returns a single column from the next row of a result set. + * + * @param int $col OPTIONAL Position of the column to fetch. + * @return string + * @throws StatementException + */ + public function fetchColumn($col = 0); + + /** + * Fetches the next row and returns it as an object. + * + * @param string $class OPTIONAL Name of the class to create. + * @param array $config OPTIONAL Constructor arguments for the class. + * @return mixed One object instance of the specified class. + * @throws StatementException + */ + public function fetchObject($class = 'stdClass', array $config = array()); + + /** + * Retrieve a statement attribute. + * + * @param string $key Attribute name. + * @return mixed Attribute value. + * @throws StatementException + */ + public function getAttribute($key); + + /** + * Retrieves the next rowset (result set) for a SQL statement that has + * multiple result sets. An example is a stored procedure that returns + * the results of multiple queries. + * + * @return bool + * @throws StatementException + */ + public function nextRowset(); + + /** + * Returns the number of rows affected by the execution of the + * last INSERT, DELETE, or UPDATE statement executed by this + * statement object. + * + * @return int The number of rows affected. + * @throws StatementException + */ + public function rowCount(); + + /** + * Set a statement attribute. + * + * @param string $key Attribute name. + * @param mixed $val Attribute value. + * @return bool + * @throws StatementException + */ + public function setAttribute($key, $val); + + /** + * Set the default fetch mode for this statement. + * + * @param int $mode The fetch mode. + * @return bool + * @throws StatementException + */ + public function setFetchMode($mode); +} diff --git a/vendor/gipfl/zfdbstore/composer.json b/vendor/gipfl/zfdbstore/composer.json new file mode 100644 index 0000000..dc84939 --- /dev/null +++ b/vendor/gipfl/zfdbstore/composer.json @@ -0,0 +1,16 @@ +{ + "name": "gipfl/zfdbstore", + "description": "Storable class helpers for ZfDb", + "type": "library", + "require": { + "php": ">=5.6" + }, + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\ZfDbStore\\": "src" + } + } +} diff --git a/vendor/gipfl/zfdbstore/src/BaseStore.php b/vendor/gipfl/zfdbstore/src/BaseStore.php new file mode 100644 index 0000000..176ec33 --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/BaseStore.php @@ -0,0 +1,112 @@ +<?php + +namespace gipfl\ZfDbStore; + +use Evenement\EventEmitterTrait; +use RuntimeException; + +/** + * Class BaseStore + * + * This class handles creation/update/delete of $storable + * and save records them into a log table in the DB + */ +abstract class BaseStore implements Store +{ + use EventEmitterTrait; + + /** + * If a new element is created, it stores it into the DB + * and emits the "insert" event, in order to create the corresponding activity log + * + * If an element is updated, it updates the DB record and + * emits the "update" event + * + * @param StorableInterface $storable + * @return bool Whether the store() applied any change to the stored object + */ + public function store(StorableInterface $storable) + { + $affected = false; + + if ($storable->isNew()) { + $this->insertIntoStore($storable); + $this->emit('insert', [$storable]); + $affected = true; + } else { + if ($storable->isModified() && $this->updateStore($storable)) { + $this->emit('update', [$storable]); + $affected = true; + } + } + $storable->setStored(); + + return $affected; + } + + /** + * If a new element is deleted, it deletes the record from the DB + * and emits the "delete" event, in order to create the corresponding activity log + * + * @param StorableInterface $storable + * @return bool + */ + public function delete(StorableInterface $storable) + { + if ($this->deleteFromStore($storable)) { + $this->emit('delete', [$storable]); + return true; + } + + return false; + } + + /** + * Loads $storable by it's key property/properties + * + * @param StorableInterface $storable + * @param $key + * @return StorableInterface + */ + abstract protected function loadFromStore(StorableInterface $storable, $key); + + /** + * Deletes this record from the store + * + * @param StorableInterface $storable + */ + abstract protected function deleteFromStore(StorableInterface $storable); + + /** + * Inserts the $storable, refreshes the object in case storing implies + * changes (like auto-incremental IDs) + * + * @param StorableInterface $storable + * @return bool + * @throws \gipfl\ZfDb\Adapter\Exception\AdapterException + * @throws \gipfl\ZfDb\Statement\Exception\StatementException + * @throws \Zend_Db_Adapter_Exception + */ + abstract protected function insertIntoStore(StorableInterface $storable); + abstract protected function updateStore(StorableInterface $storable); + + /** + * Load $storable from DB + * + * @param array|string $key + * @param string $className + * @return StorableInterface + */ + public function load($key, $className) + { + $storable = new $className(); + if ($storable instanceof StorableInterface) { + $storable = $storable::create(); + return $this->loadFromStore($storable, $key); + } else { + throw new RuntimeException( + get_class($this) . "::load: '$className' is not a StorableInterface implementation" + ); + } + } +} diff --git a/vendor/gipfl/zfdbstore/src/DbStorable.php b/vendor/gipfl/zfdbstore/src/DbStorable.php new file mode 100644 index 0000000..4bd1783 --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/DbStorable.php @@ -0,0 +1,79 @@ +<?php + +namespace gipfl\ZfDbStore; + +use RuntimeException; + +/** + * DbStorable + * + * This trait provides all you need to create an object implementing the + * DbStorableInterface + */ +trait DbStorable +{ + use Storable; + + // protected $tableName; + + public function getTableName() + { + if (isset($this->tableName)) { + return $this->tableName; + } else { + throw new RuntimeException('A DbStorable needs a tableName'); + } + } + + public function hasAutoIncKey() + { + return $this->getAutoIncKeyName() !== null; + } + + public function getAutoIncKeyName() + { + if (isset($this->autoIncKeyName)) { + return $this->autoIncKeyName; + } else { + return null; + } + } + + protected function requireAutoIncKeyName() + { + $key = $this->getAutoIncKeyName(); + if ($key === null) { + throw new RuntimeException('This DbStorable has no autoinc key'); + } + + return $key; + } + + public function getAutoIncId() + { + $key = $this->requireAutoIncKeyName(); + if (isset($this->properties[$key])) { + return (int) $this->properties[$key]; + } + + return null; + } + + protected function forgetAutoIncId() + { + $key = $this->requireAutoIncKeyName(); + if (isset($this->properties[$key])) { + $this->properties[$key] = null; + } + + return $this; + } + + public function __invoke($properties = []) + { + $storable = new static(); + $storable->setProperties($properties); + + return $storable; + } +} diff --git a/vendor/gipfl/zfdbstore/src/DbStorableInterface.php b/vendor/gipfl/zfdbstore/src/DbStorableInterface.php new file mode 100644 index 0000000..ba9a49f --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/DbStorableInterface.php @@ -0,0 +1,36 @@ +<?php + +namespace gipfl\ZfDbStore; + +interface DbStorableInterface extends StorableInterface +{ + /** + * The table where this Storable will be loaded from and stored to + * + * @return string + */ + public function getTableName(); + + /** + * Whether this Storable has an auto-incrementing key column + * @return bool + */ + public function hasAutoIncKey(); + + /** + * Returns the name of the auto-incrementing key column + * + * @return string + */ + public function getAutoIncKeyName(); + + /** + * Get the AutoInc value if set + * + * Should throw and Exception in case no such key has been defined. This + * will return null for unstored DbStorables + * + * @return int|null + */ + public function getAutoIncId(); +} diff --git a/vendor/gipfl/zfdbstore/src/NotFoundError.php b/vendor/gipfl/zfdbstore/src/NotFoundError.php new file mode 100644 index 0000000..fb2413e --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/NotFoundError.php @@ -0,0 +1,9 @@ +<?php + +namespace gipfl\ZfDbStore; + +use Exception; + +class NotFoundError extends Exception +{ +} diff --git a/vendor/gipfl/zfdbstore/src/Storable.php b/vendor/gipfl/zfdbstore/src/Storable.php new file mode 100644 index 0000000..9980efd --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/Storable.php @@ -0,0 +1,323 @@ +<?php + +namespace gipfl\ZfDbStore; + +use InvalidArgumentException; +use RuntimeException; + +/** + * Trait Storable + * + * This trait implements a generic trait used for storing + * information about users activity (i.e. creation of new elements, + * update/delete existing records) + * + * Each storable is characterized by: + * - $defaultProperties (array of properties set by default) + * - $modifiedProperties (array of properties modified by the user) + * - $storedProperties (array of properties loaded from the DB) + * - key property represents the primary key in the DB + */ +trait Storable +{ + /** @var null|array */ + protected $storedProperties; + + ///** @var array */ + //protected $defaultProperties = []; + + /** @var array */ + protected $modifiedProperties = []; + + /** @var array */ + protected $properties = []; + + ///** @var string|array */ + //protected $keyProperty; + + /** + * If a $storable has no stored properties it means that + * it is a new element -> the user is creating it right now + * + * @return bool + */ + public function isNew() + { + return null === $this->storedProperties; + } + + /** + * This function returns the key property (it can be an array of properties) of the $storable + * i.e. it returns the primary key in the case of DB object + * + * @return array|mixed + */ + public function getKey() + { + $property = $this->getKeyProperty(); + if (is_string($property)) { + return $this->get($property); + } else { + return $this->getProperties($property); + } + } + + /** + * @return string|array + */ + public function getKeyProperty() + { + if (isset($this->keyProperty)) { + return $this->keyProperty; + } else { + throw new RuntimeException('A storable needs a key property.'); + } + } + + /** + * Create a $storable setting its properties + * + * @param array $properties + * @return static + */ + public static function create(array $properties = []) + { + $storable = new static(); + $storable->properties = $storable->getDefaultProperties(); + $storable->setProperties($properties); + + return $storable; + } + + /** + * Loads an already existing $storable + * + * @param Store $store + * @param $key + * @return mixed + */ + public static function load(Store $store, $key) + { + return $store->load($key, get_called_class()); + return $store->load(get_called_class(), $key); + } + + /** + * Returns the value of $property (if this property exists) + * + * @param $property + * @return mixed + */ + public function get($property, $default = null) + { + $this->assertPropertyExists($property); + + if (array_key_exists($property, $this->properties)) { + if ($this->properties[$property] === null) { + return $default; + } else { + return $this->properties[$property]; + } + } else { + return $default; + } + } + + /** + * Returns the array of values corresponding to the requested array of properties + * + * @param array|null $properties + * @return array + */ + public function getProperties(array $properties = null) + { + if ($properties === null) { + $properties = array_keys($this->properties); + } + + $result = []; + foreach ($properties as $property) { + $result[$property] = $this->get($property); + } + + return $result; + } + + /** + * Returns the array of properties modified by the user + * + * @return array + */ + public function getModifiedProperties() + { + return $this->getProperties($this->listModifiedProperties()); + } + + /** + * Returns the array of stored properties + * It can be used only in case of already existing $storable + */ + public function getStoredProperties() + { + if ($this->isNew()) { + throw new RuntimeException( + 'Trying to access stored properties of an unstored Storable' + ); + } + + return $this->storedProperties; + } + + /** + * Set the value of a given property + * + * @param $property + * @param $value + * @return bool + */ + public function set($property, $value) + { + $this->assertPropertyExists($property); + + if ($value === $this->get($property)) { + return false; + } + + $this->properties[$property] = $value; + + if ($this->storedProperties !== null && $this->storedProperties[$property] === $value) { + $this->resetModifiedProperty($property); + } else { + $this->setModifiedProperty($property); + } + + return true; + } + + /** + * Initialize the stored property at the first loading of the $storable element + * + * @param $property + * @param $value + */ + public function setStoredProperty($property, $value) + { + $this->assertPropertyExists($property); + + $this->storedProperties[$property] = $value; + $this->properties[$property] = $value; + unset($this->modifiedProperties[$property]); + } + + /** + * Set array of values for the given array of properties + * + * @param array $properties + * @return $this + */ + public function setProperties(array $properties) + { + foreach ($properties as $property => $value) { + $this->set($property, $value); + } + + return $this; + } + + /** + * Initialize the stored property array + * + * @param array $properties + * @return $this + */ + public function setStoredProperties(array $properties) + { + foreach ($properties as $property => $value) { + $this->setStoredProperty($property, $value); + } + + return $this; + } + + /** + * @param $property + */ + public function assertPropertyExists($property) + { + if (! $this->hasProperty($property)) { + throw new InvalidArgumentException(sprintf( + "Trying to access invalid property '%s'", + $property + )); + } + } + + /** + * @param $property + * @return bool + */ + public function hasProperty($property) + { + return array_key_exists($property, $this->defaultProperties); + } + + /** + * @param $property + */ + private function setModifiedProperty($property) + { + $this->modifiedProperties[$property] = true; + } + + /** + * @param $property + */ + private function resetModifiedProperty($property) + { + unset($this->modifiedProperties[$property]); + } + + /** + * Check if $storable has changed, + * if not the $modifiedProperties array is empty + * + * @return bool + */ + public function isModified() + { + return !empty($this->modifiedProperties); + } + + /** + * @return mixed + */ + public function getDefaultProperties() + { + if (isset($this->defaultProperties)) { + return $this->defaultProperties; + } else { + throw new RuntimeException('A storable needs default properties.'); + } + } + + /** + * Get the array key of the modifies properties + * + * @return array + */ + public function listModifiedProperties() + { + return array_keys($this->modifiedProperties); + } + + /** + * @return $this + */ + public function setStored() + { + $this->storedProperties = $this->properties; + $this->modifiedProperties = []; + + return $this; + } +} diff --git a/vendor/gipfl/zfdbstore/src/StorableInterface.php b/vendor/gipfl/zfdbstore/src/StorableInterface.php new file mode 100644 index 0000000..8ae49d2 --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/StorableInterface.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\ZfDbStore; + +interface StorableInterface +{ + public function isNew(); + + public function getKey(); + + public function getKeyProperty(); + + public static function create(array $properties = []); + + public static function load(Store $store, $key); + + public function get($property); + + public function getProperties(array $properties = null); + + public function hasProperty($property); + + public function getModifiedProperties(); + + public function getStoredProperties(); + + public function set($property, $value); + + public function setStoredProperty($property, $value); + + public function setProperties(array $properties); + + public function setStoredProperties(array $properties); + + public function assertPropertyExists($property); + + public function isModified(); + + public function getDefaultProperties(); + + public function listModifiedProperties(); + + public function setStored(); +} diff --git a/vendor/gipfl/zfdbstore/src/Store.php b/vendor/gipfl/zfdbstore/src/Store.php new file mode 100644 index 0000000..cdc7ab1 --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/Store.php @@ -0,0 +1,24 @@ +<?php + +namespace gipfl\ZfDbStore; + +interface Store +{ + /** + * Function used for saving changes and log the activity. + * To be extended as needed (see BaseStore.php) + * + * @param StorableInterface $storable + * @return mixed + */ + public function store(StorableInterface $storable); + + /** + * @param array|string $key + * @param string $className + * @return StorableInterface + */ + public function load($key, $className); + public function exists(StorableInterface $storable); + public function delete(StorableInterface $storable); +} diff --git a/vendor/gipfl/zfdbstore/src/ZfDbStore.php b/vendor/gipfl/zfdbstore/src/ZfDbStore.php new file mode 100644 index 0000000..d34541c --- /dev/null +++ b/vendor/gipfl/zfdbstore/src/ZfDbStore.php @@ -0,0 +1,241 @@ +<?php + +namespace gipfl\ZfDbStore; + +use InvalidArgumentException; +use RuntimeException; +use gipfl\ZfDb\Adapter\Adapter; +use Zend_Db_Adapter_Abstract as ZfDb; +use function array_key_exists; +use function assert; +use function implode; +use function is_array; +use function is_string; +use function method_exists; + +/** + * Class DbStore + * + * Extends BaseStore for DB object + */ +class ZfDbStore extends BaseStore +{ + /** @var Adapter|ZfDb */ + protected $db; + + /** + * ZfDbStore constructor. + * @param Adapter|ZfDb $db + */ + public function __construct($db) + { + if ($db instanceof Adapter || $db instanceof ZfDb) { + $this->db = $db; + } else { + throw new InvalidArgumentException('ZfDb Adapter is required'); + } + } + + /** + * @return Adapter|ZfDb + */ + public function getDb() + { + return $this->db; + } + + /** + * Checks whether the passed $storable already exists in the DB + * + * @param DbStorableInterface $storable + * @return bool + */ + public function exists(StorableInterface $storable) + { + return (int) $this->db->fetchOne( + $this->db + ->select() + ->from($this->getTableName($storable), '(1)') + ->where($this->createWhere($storable)) + ) === 1; + } + + /** + * @param DbStorableInterface $storable + * @param string|null $keyColumn + * @param string|null $labelColumn + * @return array + */ + public function enum(StorableInterface $storable, $keyColumn = null, $labelColumn = null) + { + assert($storable instanceof DbStorableInterface); + if ($keyColumn === null) { + $key = $storable->getKeyProperty(); + if (is_array($key)) { + if ($storable->hasAutoIncKey()) { + $key = $storable->getAutoIncKeyName(); + } else { + throw new InvalidArgumentException( + 'Cannot provide an enum for a multi-key column' + ); + } + } + } else { + $key = $keyColumn; + } + + if ($labelColumn === null) { + if (method_exists($storable, 'getDisplayColumn')) { + $label = $storable->getDisplayColumn(); + } else { + $label = $storable->getKeyProperty(); + if (is_array($label)) { + $label = $key; + } + } + } else { + $label = $labelColumn; + } + + $columns = [ + 'key_col' => $key, + 'label_col' => $label + ]; + + $query = $this->db->select()->from( + $this->getTableName($storable), + $columns + ); + + return $this->db->fetchPairs($query); + } + + protected function insertIntoStore(StorableInterface $storable) + { + assert($storable instanceof DbStorableInterface); + $result = $this->db->insert( + $this->getTableName($storable), + $storable->getProperties() + ); + /** @var DbStorable $storable */ + if ($storable->hasAutoIncKey()) { + $storable->set( + $storable->getAutoIncKeyName(), + $this->db->lastInsertId($this->getTableName($storable)) + ); + } + + return $result > 0; + } + + protected function updateStore(StorableInterface $storable) + { + assert($storable instanceof DbStorableInterface); + $this->db->update( + $this->getTableName($storable), + $storable->getProperties(), + $this->createWhere($storable) + ); + + return true; + } + + protected function deleteFromStore(StorableInterface $storable) + { + assert($storable instanceof DbStorableInterface); + return $this->db->delete( + $this->getTableName($storable), + $this->createWhere($storable) + ); + } + + protected function loadFromStore(StorableInterface $storable, $key) + { + assert($storable instanceof DbStorableInterface); + $keyColumn = $storable->getKeyProperty(); + $select = $this->db->select()->from($this->getTableName($storable)); + + if (is_string($keyColumn)) { + $select->where("$keyColumn = ?", $key); + } else { + foreach ($keyColumn as $column) { + if (array_key_exists($column, $key)) { + $select->where("$column = ?", $key[$column]); + } else { + throw new RuntimeException('Multicolumn key required, got no %s', $column); + } + } + } + + $result = $this->db->fetchAll($select); + // TODO: properties should be changed in storeProperties + // when you load the element from db before changing it. + if (empty($result)) { + throw new NotFoundError('Not found: ' . $this->describeKey($storable, $key)); + } + + if (count($result) > 1) { + throw new NotFoundError(sprintf( + 'One row expected, got %s: %s', + count($result), + $this->describeKey($storable, $key) + )); + } + + $storable->setStoredProperties((array) $result[0]); + + return $storable; + } + + protected function describeKey(StorableInterface $storable, $key) + { + + assert($storable instanceof DbStorableInterface); + $keyColumn = $storable->getKeyProperty(); + if (is_string($keyColumn)) { + return (string) $key; + } + + $parts = []; + foreach ($keyColumn as $column) { + if (array_key_exists($column, $key)) { + $parts[$column] = $key[$column]; + } else { + $parts[$column] = '?'; + } + } + return implode(', ', $parts); + } + + /** + * Returns $storable table name + * + * @param DbStorableInterface $storable + * @return string + */ + protected function getTableName(DbStorableInterface $storable) + { + return $storable->getTableName(); + } + + /** + * @param DbStorableInterface $storable + * @return string + */ + protected function createWhere($storable) + { + $where = []; + foreach ((array) $storable->getKeyProperty() as $key) { + $value = $storable->get($key); + // TODO, eventually: + // $key = $this->db->quoteIdentifier($key); + if ($value === null) { + $where[] = "$key IS NULL"; + } else { + $where[] = $this->db->quoteInto("$key = ?", $value); + } + } + + return implode(' AND ', $where); + } +} |