<?php

/*
 * This file is part of the API Platform project.
 *
 * (c) Kévin Dunglas <dunglas@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace ApiPlatform\SchemaGenerator\AnnotationGenerator;

use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * Constraint annotation generator.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
final class ConstraintAnnotationGenerator extends AbstractAnnotationGenerator
{
    /**
     * {@inheritdoc}
     */
    public function generateFieldAnnotations(string $className, string $fieldName): array
    {
        $field = $this->classes[$className]['fields'][$fieldName];

        if ($field['isId']) {
            if ('uuid' === $this->config['id']['generationStrategy']) {
                return ['@Assert\Uuid'];
            }

            return [];
        }

        $asserts = [];
        if (!$field['isArray']) {
            switch ($field['range']) {
                case 'URL':
                    $asserts[] = '@Assert\Url';
                    break;
                case 'Date':
                    $asserts[] = '@Assert\Date';
                    break;
                case 'DateTime':
                    $asserts[] = '@Assert\DateTime';
                    break;
                case 'Time':
                    $asserts[] = '@Assert\Time';
                    break;
            }

            if (isset($field['resource']) && 'email' === $field['resource']->localName()) {
                $asserts[] = '@Assert\Email';
            }

            if (!$asserts && $this->config['validator']['assertType']) {
                $phpType = $this->toPhpType($field);
                if (in_array($phpType, ['boolean', 'float', 'integer', 'string'], true)) {
                    $asserts[] = sprintf('@Assert\Type(type="%s")', $phpType);
                }
            }
        }

        if (!$field['isNullable']) {
            $asserts[] = '@Assert\NotNull';
        }

        if ($field['isEnum']) {
            $assert = sprintf('@Assert\Choice(callback={"%s", "toArray"}', $field['range']);

            if ($field['isArray']) {
                $assert .= ', multiple=true';
            }

            $assert .= ')';

            $asserts[] = $assert;
        }

        return $asserts;
    }

    /**
     * {@inheritdoc}
     */
    public function generateUses(string $className): array
    {
        if ($this->classes[$className]['isEnum']) {
            return [];
        }

        $uses = [];
        $uses[] = 'Symfony\Component\Validator\Constraints as Assert';
        $uses[] = UniqueEntity::class;

        foreach ($this->classes[$className]['fields'] as $field) {
            if ($field['isEnum']) {
                $enumClass = $this->classes[$field['range']];
                $enumNamespace = isset($enumClass['namespaces']['class']) && $enumClass['namespaces']['class'] ? $enumClass['namespaces']['class'] : $this->config['namespaces']['enum'];
                $use = sprintf('%s\%s', $enumNamespace, $field['range']);

                if (!in_array($use, $uses, true)) {
                    $uses[] = $use;
                }
            }
        }

        return $uses;
    }

    /**
     * {@inheritdoc}
     */
    public function generateClassAnnotations(string $className): array
    {
        if ($this->classes[$className]['isEnum']) {
            return [];
        }

        $annotation = [];
        $uniqueFields = [];

        foreach ($this->classes[$className]['fields'] as $field) {
            if (false === $field['isUnique']) {
                continue;
            }

            $uniqueFields[] = $field['name'];
        }

        if (!$uniqueFields) {
            return [];
        }

        if (1 === count($uniqueFields)) {
            $annotation[] = sprintf('@UniqueEntity("%s")', $uniqueFields[0]);
        } else {
            $annotation[] = sprintf('@UniqueEntity(fields={"%s"})', implode('","', $uniqueFields));
        }

        return $annotation;
    }
}


<?php

/*
 * This file is part of the API Platform project.
 *
 * (c) Kévin Dunglas <dunglas@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace ApiPlatform\SchemaGenerator\AnnotationGenerator;

use Symfony\Component\Serializer\Annotation\Groups;

/**
 * Symfony Serializer Groups annotation generator.
 *
 * @author Youssef El Montaser <youssef@elmontaser.com>
 * @author Kévin Dunglas <dunglas@gmail.com>
 *
 * @see https://symfony.com/doc/master/components/serializer.html
 */
final class SerializerGroupsAnnotationGenerator extends AbstractAnnotationGenerator
{
    /**
     * {@inheritdoc}
     */
    public function generateFieldAnnotations(string $className, string $fieldName): array
    {
        if (null === $field = $this->config['types'][$className]['properties'] ?? null) {
            return [];
        }

        if (false === $this->classes[$className]['fields'][$fieldName]['isId'] && $groups = $field[$fieldName]['groups'] ?? false) {
            return [sprintf('@Groups({"%s"})', implode('", "', $groups))];
        }

        return [];
    }

    /**
     * {@inheritdoc}
     */
    public function generateUses(string $className): array
    {
        return [Groups::class];
    }
}


<?php

/*
 * This file is part of the API Platform project.
 *
 * (c) Kévin Dunglas <dunglas@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace ApiPlatform\SchemaGenerator;

use ApiPlatform\SchemaGenerator\AnnotationGenerator\ApiPlatformCoreAnnotationGenerator;
use ApiPlatform\SchemaGenerator\AnnotationGenerator\ConstraintAnnotationGenerator;
use ApiPlatform\SchemaGenerator\AnnotationGenerator\DoctrineOrmAnnotationGenerator;
use ApiPlatform\SchemaGenerator\AnnotationGenerator\PhpDocAnnotationGenerator;
use ApiPlatform\SchemaGenerator\AnnotationGenerator\SerializerGroupsAnnotationGenerator;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * Types Generator Configuration.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
final class TypesGeneratorConfiguration implements ConfigurationInterface
{
    /**
     * @see https://schema.org/docs/schema_org_rdfa.html
     */
    public const SCHEMA_ORG_RDFA_URL = __DIR__.'/../data/schema.rdfa';

    /**
     * @see https://purl.org/goodrelations/v1.owl
     */
    public const GOOD_RELATIONS_OWL_URL = __DIR__.'/../data/v1.owl';
    public const SCHEMA_ORG_NAMESPACE = 'http://schema.org/';

    private $defaultPrefix;

    public function __construct(?string $defaultPrefix = null)
    {
        $this->defaultPrefix = $defaultPrefix;
    }

    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $namespacePrefix = $this->defaultPrefix ?? 'AppBundle\\';

        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('config');
        $rootNode
            ->children()
                ->arrayNode('rdfa')
                    ->info('RDFa files')
                    ->defaultValue([['uri' => self::SCHEMA_ORG_RDFA_URL, 'format' => 'rdfa']])
                    ->beforeNormalization()
                        ->ifArray()
                        ->then(function (array $v) {
                            return array_map(
                                function ($rdfa) {
                                    return is_scalar($rdfa) ? ['uri' => $rdfa, 'format' => null] : $rdfa;
                                },
                                $v
                            );
                        })
                    ->end()
                    ->arrayPrototype()
                        ->children()
                            ->scalarNode('uri')->defaultValue(self::SCHEMA_ORG_RDFA_URL)->info('RDFa URI to use')->example('https://schema.org/docs/schema_org_rdfa.html')->end()
                            ->scalarNode('format')->defaultNull()->info('RDFa URI data format')->example('rdfxml')->end()
                        ->end()
                    ->end()
                ->end()
                ->arrayNode('relations')
                    ->info('OWL relation files to use')
                    ->example('https://purl.org/goodrelations/v1.owl')
                    ->defaultValue([self::GOOD_RELATIONS_OWL_URL])
                    ->prototype('scalar')->end()
                ->end()
                ->booleanNode('debug')->defaultFalse()->info('Debug mode')->end()
                ->arrayNode('id')
                    ->addDefaultsIfNotSet()
                    ->info('IDs configuration')
                    ->children()
                        ->booleanNode('generate')->defaultTrue()->info('Automatically add an id field to entities')->end()
                        ->enumNode('generationStrategy')->defaultValue('auto')->values(['auto', 'none', 'uuid', 'mongoid'])->info('The ID generation strategy to use ("none" to not let the database generate IDs).')->end()
                        ->booleanNode('writable')->defaultFalse()->info('Is the ID writable? Only applicable if "generationStrategy" is "uuid".')->end()
                    ->end()
                ->end()
                ->booleanNode('useInterface')->defaultFalse()->info('Generate interfaces and use Doctrine\'s Resolve Target Entity feature')->end()
                ->booleanNode('checkIsGoodRelations')->defaultFalse()->info('Emit a warning if a property is not derived from GoodRelations')->end()
                ->scalarNode('header')->defaultFalse()->info('A license or any text to use as header of generated files')->example('// (c) Kévin Dunglas <dunglas@gmail.com>')->end()
                ->arrayNode('namespaces')
                    ->addDefaultsIfNotSet()
                    ->info('PHP namespaces')
                    ->children()
                        ->scalarNode('prefix')->defaultValue($this->defaultPrefix)->info('The global namespace\'s prefix')->example('App\\')->end()
                        ->scalarNode('entity')->defaultValue("{$namespacePrefix}Entity")->info('The namespace of the generated entities')->example('App\Entity')->end()
                        ->scalarNode('enum')->defaultValue("{$namespacePrefix}Enum")->info('The namespace of the generated enumerations')->example('App\Enum')->end()
                        ->scalarNode('interface')->defaultValue("{$namespacePrefix}Model")->info('The namespace of the generated interfaces')->example('App\Model')->end()
                    ->end()
                ->end()
                ->arrayNode('doctrine')
                    ->addDefaultsIfNotSet()
                    ->info('Doctrine')
                    ->children()
                        ->booleanNode('useCollection')->defaultTrue()->info('Use Doctrine\'s ArrayCollection instead of standard arrays')->end()
                        ->scalarNode('resolveTargetEntityConfigPath')->defaultNull()->info('The Resolve Target Entity Listener config file pass')->end()
                    ->end()
                ->end()
                ->arrayNode('validator')
                    ->addDefaultsIfNotSet()
                    ->info('Symfony Validator Component')
                    ->children()
                        ->booleanNode('assertType')->defaultFalse()->info('Generate @Assert\Type annotation')->end()
                    ->end()
                ->end()
                ->scalarNode('author')->defaultFalse()->info('The value of the phpDoc\'s @author annotation')->example('Kévin Dunglas <dunglas@gmail.com>')->end()
                ->enumNode('fieldVisibility')->values(['private', 'protected', 'public'])->defaultValue('private')->cannotBeEmpty()->info('Visibility of entities fields')->end()
                ->booleanNode('accessorMethods')->defaultTrue()->info('Set this flag to false to not generate getter, setter, adder and remover methods')->end()
                ->booleanNode('fluentMutatorMethods')->defaultFalse()->info('Set this flag to true to generate fluent setter, adder and remover methods')->end()
                ->arrayNode('types')
                    ->beforeNormalization()
                        ->always()
                        ->then(function ($v) {
                            foreach ($v as $key => $type) {
                                if (!isset($type['properties'])) {
                                    $v[$key]['allProperties'] = true;
                                }
                            }

                            return $v;
                        })
                    ->end()
                    ->info('Schema.org\'s types to use')
                    ->useAttributeAsKey('id')
                    ->arrayPrototype()
                        ->children()
                            ->scalarNode('vocabularyNamespace')->defaultValue(self::SCHEMA_ORG_NAMESPACE)->info('Namespace of the vocabulary the type belongs to.')->end()
                            ->booleanNode('abstract')->defaultNull()->info('Is the class abstract? (null to guess)')->end()
                            ->booleanNode('embeddable')->defaultFalse()->info('Is the class embeddable?')->end()
                            ->arrayNode('namespaces')
                                ->addDefaultsIfNotSet()
                                ->info('Type namespaces')
                                ->children()
                                    ->scalarNode('class')->defaultNull()->info('The namespace for the generated class (override any other defined namespace)')->end()
                                    ->scalarNode('interface')->defaultNull()->info('The namespace for the generated interface (override any other defined namespace)')->end()
                                ->end()
                            ->end()
                            ->arrayNode('doctrine')
                                ->addDefaultsIfNotSet()
                                ->children()
                                    ->scalarNode('inheritanceMapping')->defaultNull()->info('The Doctrine inheritance mapping type (override the guessed one)')->end()
                                ->end()
                            ->end()
                            ->scalarNode('parent')->defaultFalse()->info('The parent class, set to false for a top level class')->end()
                            ->scalarNode('guessFrom')->defaultValue('Thing')->info('If declaring a custom class, this will be the class from which properties type will be guessed')->end()
                            ->booleanNode('allProperties')->defaultFalse()->info('Import all existing properties')->end()
                            ->arrayNode('properties')
                                ->info('Properties of this type to use')
                                ->useAttributeAsKey('id')
                                ->arrayPrototype()
                                    ->addDefaultsIfNotSet()
                                    ->children()
                                        ->scalarNode('range')->defaultNull()->info('The property range')->example('Offer')->end()
                                        ->scalarNode('relationTableName')->defaultNull()->info('The relation table name')->example('organization_member')->end()
                                        ->enumNode('cardinality')->defaultValue(CardinalitiesExtractor::CARDINALITY_UNKNOWN)->values([
                                            CardinalitiesExtractor::CARDINALITY_0_1,
                                            CardinalitiesExtractor::CARDINALITY_0_N,
                                            CardinalitiesExtractor::CARDINALITY_1_1,
                                            CardinalitiesExtractor::CARDINALITY_1_N,
                                            CardinalitiesExtractor::CARDINALITY_N_0,
                                            CardinalitiesExtractor::CARDINALITY_N_1,
                                            CardinalitiesExtractor::CARDINALITY_N_N,
                                            CardinalitiesExtractor::CARDINALITY_UNKNOWN,
                                        ])->end()
                                        ->scalarNode('ormColumn')->defaultNull()->info('The doctrine column annotation content')->example('type="decimal", precision=5, scale=1, options={"comment" = "my comment"}')->end()
                                        ->arrayNode('groups')
                                            ->info('Symfony Serialization Groups')
                                            ->prototype('scalar')->end()
                                        ->end()
                                        ->booleanNode('readable')->defaultTrue()->info('Is the property readable?')->end()
                                        ->booleanNode('writable')->defaultTrue()->info('Is the property writable?')->end()
                                        ->booleanNode('nullable')->defaultTrue()->info('Is the property nullable?')->end()
                                        ->booleanNode('unique')->defaultFalse()->info('The property unique')->end()
                                        ->booleanNode('embedded')->defaultFalse()->info('Is the property embedded?')->end()
                                        ->booleanNode('columnPrefix')->defaultFalse()->info('The property columnPrefix')->end()
                                    ->end()
                                ->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
                ->arrayNode('annotationGenerators')
                    ->info('Annotation generators to use')
                    ->defaultValue([
                        PhpDocAnnotationGenerator::class,
                        DoctrineOrmAnnotationGenerator::class,
                        ApiPlatformCoreAnnotationGenerator::class,
                        ConstraintAnnotationGenerator::class,
                        SerializerGroupsAnnotationGenerator::class,
                    ])
                    ->prototype('scalar')->end()
                ->end()
            ->end();

        return $treeBuilder;
    }
}


<?php
namespace samdark\sitemap;

use XMLWriter;

/**
 * A class for generating Sitemaps (http://www.sitemaps.org/)
 *
 * @author Alexander Makarov <sam@rmcreative.ru>
 */
class Sitemap
{
    const ALWAYS = 'always';
    const HOURLY = 'hourly';
    const DAILY = 'daily';
    const WEEKLY = 'weekly';
    const MONTHLY = 'monthly';
    const YEARLY = 'yearly';
    const NEVER = 'never';

    /**
     * @var integer Maximum allowed number of URLs in a single file.
     */
    private $maxUrls = 50000;

    /**
     * @var integer number of URLs added
     */
    private $urlsCount = 0;

    /**
     * @var integer Maximum allowed number of bytes in a single file.
     */
    private $maxBytes = 10485760;

    /**
     * @var integer number of bytes already written to the current file, before compression
     */
    private $byteCount = 0;

    /**
     * @var string path to the file to be written
     */
    private $filePath;

    /**
     * @var integer number of files written
     */
    private $fileCount = 0;

    /**
     * @var array path of files written
     */
    private $writtenFilePaths = array();

    /**
     * @var integer number of URLs to be kept in memory before writing it to file
     */
    private $bufferSize = 10;

    /**
     * @var bool if XML should be indented
     */
    private $useIndent = true;

    /**
     * @var bool if should XHTML namespace be specified
     * Useful for multi-language sitemap to point crawler to alternate language page via xhtml:link tag.
     * @see https://support.google.com/webmasters/answer/2620865?hl=en
     */
    private $useXhtml = false;

    /**
     * @var array valid values for frequency parameter
     */
    private $validFrequencies = array(
        self::ALWAYS,
        self::HOURLY,
        self::DAILY,
        self::WEEKLY,
        self::MONTHLY,
        self::YEARLY,
        self::NEVER
    );

    /**
     * @var bool whether to gzip the resulting files or not
     */
    private $useGzip = false;

    /**
     * @var WriterInterface that does the actual writing
     */
    private $writerBackend;

    /**
     * @var XMLWriter
     */
    private $writer;

    /**
     * @param string $filePath path of the file to write to
     * @param bool $useXhtml is XHTML namespace should be specified
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($filePath, $useXhtml = false)
    {
        $dir = dirname($filePath);
        if (!is_dir($dir)) {
            throw new \InvalidArgumentException(
                "Please specify valid file path. Directory not exists. You have specified: {$dir}."
            );
        }

        $this->filePath = $filePath;
        $this->useXhtml = $useXhtml;
    }

    /**
     * Get array of generated files
     * @return array
     */
    public function getWrittenFilePath()
    {
        return $this->writtenFilePaths;
    }

    /**
     * Creates new file
     * @throws \RuntimeException if file is not writeable
     */
    private function createNewFile()
    {
        $this->fileCount++;
        $filePath = $this->getCurrentFilePath();
        $this->writtenFilePaths[] = $filePath;

        if (file_exists($filePath)) {
            $filePath = realpath($filePath);
            if (is_writable($filePath)) {
                unlink($filePath);
            } else {
                throw new \RuntimeException("File \"$filePath\" is not writable.");
            }
        }

        if ($this->useGzip) {
            if (function_exists('deflate_init') && function_exists('deflate_add')) {
                $this->writerBackend = new DeflateWriter($filePath);
            } else {
                $this->writerBackend = new TempFileGZIPWriter($filePath);
            }
        } else {
            $this->writerBackend = new PlainFileWriter($filePath);
        }

        $this->writer = new XMLWriter();
        $this->writer->openMemory();
        $this->writer->startDocument('1.0', 'UTF-8');
        $this->writer->setIndent($this->useIndent);
        $this->writer->startElement('urlset');
        $this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
        if ($this->useXhtml) {
            $this->writer->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
        }

        /*
         * XMLWriter does not give us much options, so we must make sure, that
         * the header was written correctly and we can simply reuse any <url>
         * elements that did not fit into the previous file. (See self::flush)
         */
        $this->writer->text("\n");
        $this->flush(true);
    }

    /**
     * Writes closing tags to current file
     */
    private function finishFile()
    {
        if ($this->writer !== null) {
            $this->writer->endElement();
            $this->writer->endDocument();

            /* To prevent infinite recursion through flush */
            $this->urlsCount = 0;

            $this->flush(0);
            $this->writerBackend->finish();
            $this->writerBackend = null;

            $this->byteCount = 0;
        }
    }

    /**
     * Finishes writing
     */
    public function write()
    {
        $this->finishFile();
    }

    /**
     * Flushes buffer into file
     *
     * @param int $footSize Size of the remaining closing tags
     * @throws \OverflowException
     */
    private function flush($footSize = 10)
    {
        $data = $this->writer->flush(true);
        $dataSize = mb_strlen($data, '8bit');

        /*
         * Limit the file size of each single site map
         *
         * We use a heuristic of 10 Bytes for the remainder of the file,
         * i.e. </urlset> plus a new line.
         */
        if ($this->byteCount + $dataSize + $footSize > $this->maxBytes) {
            if ($this->urlsCount <= 1) {
                throw new \OverflowException('The buffer size is too big for the defined file size limit');
            }
            $this->finishFile();
            $this->createNewFile();
        }

        $this->writerBackend->append($data);
        $this->byteCount += $dataSize;
    }

    /**
     * Takes a string and validates, if the string
     * is a valid url
     *
     * @param string $location
     * @throws \InvalidArgumentException
     */
    protected function validateLocation($location) {
        if (false === filter_var($location, FILTER_VALIDATE_URL)) {
            throw new \InvalidArgumentException(
                "The location must be a valid URL. You have specified: {$location}."
            );
        }
    }

    /**
     * Adds a new item to sitemap
     *
     * @param string|array $location location item URL
     * @param integer $lastModified last modification timestamp
     * @param string $changeFrequency change frequency. Use one of self:: constants here
     * @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5
     *
     * @throws \InvalidArgumentException
     */
    public function addItem($location, $lastModified = null, $changeFrequency = null, $priority = null)
    {
        if ($this->urlsCount >= $this->maxUrls) {
            $this->finishFile();
        }

        if ($this->writerBackend === null) {
            $this->createNewFile();
        }

        if (is_array($location)) {
            $this->addMultiLanguageItem($location, $lastModified, $changeFrequency, $priority);
        } else {
            $this->addSingleLanguageItem($location, $lastModified, $changeFrequency, $priority);
        }

        $this->urlsCount++;

        if ($this->urlsCount % $this->bufferSize === 0) {
            $this->flush();
        }
    }

    /**
     * Adds a new single item to sitemap
     *
     * @param string $location location item URL
     * @param integer $lastModified last modification timestamp
     * @param float $changeFrequency change frequency. Use one of self:: constants here
     * @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5
     *
     * @throws \InvalidArgumentException
     *
     * @see addItem
     */
    private function addSingleLanguageItem($location, $lastModified, $changeFrequency, $priority)
    {
        $this->validateLocation($location);

        $this->writer->startElement('url');

        $this->writer->writeElement('loc', $location);

        if ($lastModified !== null) {
            $this->writer->writeElement('lastmod', date('c', $lastModified));
        }

        if ($changeFrequency !== null) {
            if (!in_array($changeFrequency, $this->validFrequencies, true)) {
                throw new \InvalidArgumentException(
                    'Please specify valid changeFrequency. Valid values are: '
                    . implode(', ', $this->validFrequencies)
                    . "You have specified: {$changeFrequency}."
                );
            }

            $this->writer->writeElement('changefreq', $changeFrequency);
        }

        if ($priority !== null) {
            if (!is_numeric($priority) || $priority < 0 || $priority > 1) {
                throw new \InvalidArgumentException(
                    "Please specify valid priority. Valid values range from 0.0 to 1.0. You have specified: {$priority}."
                );
            }
            $this->writer->writeElement('priority', number_format($priority, 1, '.', ','));
        }

        $this->writer->endElement();
    }

    /**
     * Adds a multi-language item, based on multiple locations with alternate hrefs to sitemap
     *
     * @param array $locations array of language => link pairs
     * @param integer $lastModified last modification timestamp
     * @param float $changeFrequency change frequency. Use one of self:: constants here
     * @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5
     *
     * @throws \InvalidArgumentException
     *
     * @see addItem
     */
    private function addMultiLanguageItem($locations, $lastModified, $changeFrequency, $priority)
    {
        foreach ($locations as $language => $url) {
            $this->validateLocation($url);

            $this->writer->startElement('url');

            $this->writer->writeElement('loc', $url);

            if ($lastModified !== null) {
                $this->writer->writeElement('lastmod', date('c', $lastModified));
            }

            if ($changeFrequency !== null) {
                if (!in_array($changeFrequency, $this->validFrequencies, true)) {
                    throw new \InvalidArgumentException(
                        'Please specify valid changeFrequency. Valid values are: '
                        . implode(', ', $this->validFrequencies)
                        . "You have specified: {$changeFrequency}."
                    );
                }

                $this->writer->writeElement('changefreq', $changeFrequency);
            }

            if ($priority !== null) {
                if (!is_numeric($priority) || $priority < 0 || $priority > 1) {
                    throw new \InvalidArgumentException(
                        "Please specify valid priority. Valid values range from 0.0 to 1.0. You have specified: {$priority}."
                    );
                }
                $this->writer->writeElement('priority', number_format($priority, 1, '.', ','));
            }

            foreach ($locations as $hreflang => $href) {

                $this->writer->startElement('xhtml:link');
                $this->writer->startAttribute('rel');
                $this->writer->text('alternate');
                $this->writer->endAttribute();

                $this->writer->startAttribute('hreflang');
                $this->writer->text($hreflang);
                $this->writer->endAttribute();

                $this->writer->startAttribute('href');
                $this->writer->text($href);
                $this->writer->endAttribute();
                $this->writer->endElement();
            }

            $this->writer->endElement();
        }
    }

    /**
     * @return string path of currently opened file
     */
    private function getCurrentFilePath()
    {
        if ($this->fileCount < 2) {
            return $this->filePath;
        }

        $parts = pathinfo($this->filePath);
        if ($parts['extension'] === 'gz') {
            $filenameParts = pathinfo($parts['filename']);
            if (!empty($filenameParts['extension'])) {
                $parts['filename'] = $filenameParts['filename'];
                $parts['extension'] = $filenameParts['extension'] . '.gz';
            }
        }
        return $parts['dirname'] . DIRECTORY_SEPARATOR . $parts['filename'] . '_' . $this->fileCount . '.' . $parts['extension'];
    }

    /**
     * Returns an array of URLs written
     *
     * @param string $baseUrl base URL of all the sitemaps written
     * @return array URLs of sitemaps written
     */
    public function getSitemapUrls($baseUrl)
    {
        $urls = array();
        foreach ($this->writtenFilePaths as $file) {
            $urls[] = $baseUrl . pathinfo($file, PATHINFO_BASENAME);
        }
        return $urls;
    }

    /**
     * Sets maximum number of URLs to write in a single file.
     * Default is 50000.
     * @param integer $number
     */
    public function setMaxUrls($number)
    {
        $this->maxUrls = (int)$number;
    }

    /**
     * Sets maximum number of bytes to write in a single file.
     * Default is 10485760 or 10 MiB.
     * @param integer $number
     */
    public function setMaxBytes($number)
    {
        $this->maxBytes = (int)$number;
    }

    /**
     * Sets number of URLs to be kept in memory before writing it to file.
     * Default is 10.
     *
     * @param integer $number
     */
    public function setBufferSize($number)
    {
        $this->bufferSize = (int)$number;
    }

    /**
     * Sets if XML should be indented.
     * Default is true.
     *
     * @param bool $value
     */
    public function setUseIndent($value)
    {
        $this->useIndent = (bool)$value;
    }

    /**
     * Sets whether the resulting files will be gzipped or not.
     * @param bool $value
     * @throws \RuntimeException when trying to enable gzip while zlib is not available or when trying to change
     * setting when some items are already written
     */
    public function setUseGzip($value)
    {
        if ($value && !extension_loaded('zlib')) {
            throw new \RuntimeException('Zlib extension must be enabled to gzip the sitemap.');
        }
        if ($this->writerBackend !== null && $value != $this->useGzip) {
            throw new \RuntimeException('Cannot change the gzip value once items have been added to the sitemap.');
        }
        $this->useGzip = $value;
    }
}


<?php
namespace samdark\sitemap;

use XMLWriter;

/**
 * A class for generating Sitemap index (http://www.sitemaps.org/)
 *
 * @author Alexander Makarov <sam@rmcreative.ru>
 */
class Index
{
    /**
     * @var XMLWriter
     */
    private $writer;

    /**
     * @var string index file path
     */
    private $filePath;

    /**
     * @var bool whether to gzip the resulting file or not
     */
    private $useGzip = false;

    /**
     * @param string $filePath index file path
     */
    public function __construct($filePath)
    {
        $this->filePath = $filePath;
    }

    /**
     * Creates new file
     */
    private function createNewFile()
    {
        $this->writer = new XMLWriter();
        $this->writer->openMemory();
        $this->writer->startDocument('1.0', 'UTF-8');
        $this->writer->setIndent(true);
        $this->writer->startElement('sitemapindex');
        $this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
    }

    /**
     * Adds sitemap link to the index file
     *
     * @param string $location URL of the sitemap
     * @param integer $lastModified unix timestamp of sitemap modification time
     * @throws \InvalidArgumentException
     */
    public function addSitemap($location, $lastModified = null)
    {
        if (false === filter_var($location, FILTER_VALIDATE_URL)) {
            throw new \InvalidArgumentException(
                "The location must be a valid URL. You have specified: {$location}."
            );
        }

        if ($this->writer === null) {
            $this->createNewFile();
        }

        $this->writer->startElement('sitemap');
        $this->writer->writeElement('loc', $location);

        if ($lastModified !== null) {
            $this->writer->writeElement('lastmod', date('c', $lastModified));
        }
        $this->writer->endElement();
    }

    /**
     * @return string index file path
     */
    public function getFilePath()
    {
        return $this->filePath;
    }

    /**
     * Finishes writing
     */
    public function write()
    {
        if ($this->writer instanceof XMLWriter) {
            $this->writer->endElement();
            $this->writer->endDocument();
            $filePath = $this->getFilePath();
            if ($this->useGzip) {
                $filePath = 'compress.zlib://' . $filePath;
            }
            file_put_contents($filePath, $this->writer->flush());
        }
    }

    /**
     * Sets whether the resulting file will be gzipped or not.
     * @param bool $value
     * @throws \RuntimeException when trying to enable gzip while zlib is not available
     */
    public function setUseGzip($value)
    {
        if ($value && !extension_loaded('zlib')) {
            throw new \RuntimeException('Zlib extension must be installed to gzip the sitemap.');
        }
        $this->useGzip = $value;
    }
}


<?php

namespace samdark\sitemap;

/**
 * Flushes buffer into file with incremental deflating data, available in PHP 7.0+
 */
class DeflateWriter implements WriterInterface
{
    /**
     * @var resource for target file
     */
    private $file;

    /**
     * @var resource for writable incremental deflate context
     */
    private $deflateContext;

    /**
     * @param string $filename target file
     */
    public function __construct($filename)
    {
        $this->file = fopen($filename, 'ab');
        $this->deflateContext = deflate_init(ZLIB_ENCODING_GZIP);
    }

    /**
     * Deflate data in a deflate context and write it to the target file
     *
     * @param string $data
     * @param int $flushMode zlib flush mode to use for writing
     */
    private function write($data, $flushMode)
    {
        assert($this->file !== null);

        $compressedChunk = deflate_add($this->deflateContext, $data, $flushMode);
        fwrite($this->file, $compressedChunk);
    }

    /**
     * Store data in a deflate stream
     *
     * @param string $data
     */
    public function append($data)
    {
        $this->write($data, ZLIB_NO_FLUSH);
    }

    /**
     * Make sure all data was written
     */
    public function finish()
    {
        $this->write('', ZLIB_FINISH);

        $this->file = null;
        $this->deflateContext = null;
    }
}


<?php

namespace samdark\sitemap;

/**
 * Flushes buffer into temporary stream and compresses stream into a file on finish
 */
class TempFileGZIPWriter implements WriterInterface
{
    /**
     * @var string Name of target file
     */
    private $filename;

    /**
     * @var ressource for php://temp stream
     */
    private $tempFile;

    /**
     * @param string $filename target file
     */
    public function __construct($filename)
    {
        $this->filename = $filename;
        $this->tempFile = fopen('php://temp/', 'wb');
    }

    /**
     * Store data in a temporary stream/file
     *
     * @param string $data
     */
    public function append($data)
    {
        assert($this->tempFile !== null);

        fwrite($this->tempFile, $data);
    }

    /**
     * Deflate buffered data
     */
    public function finish()
    {
        assert($this->tempFile !== null);

        $file = fopen('compress.zlib://' . $this->filename, 'wb');
        rewind($this->tempFile);
        stream_copy_to_stream($this->tempFile, $file);

        fclose($file);
        fclose($this->tempFile);
        $this->tempFile = null;
    }
}


<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Crypt_GPG is a package to use GPG from PHP
 *
 * This file contains an object that handles GnuPG key generation.
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see
 * <http://www.gnu.org/licenses/>
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Michael Gauthier <mike@silverorange.com>
 * @copyright 2011-2013 silverorange
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      http://pear.php.net/package/Crypt_GPG
 * @link      http://www.gnupg.org/
 */

/**
 * Base class for GPG methods
 */
require_once 'Crypt/GPGAbstract.php';

// {{{ class Crypt_GPG_KeyGenerator

/**
 * GnuPG key generator
 *
 * This class provides an object oriented interface for generating keys with
 * the GNU Privacy Guard (GPG).
 *
 * Secure key generation requires true random numbers, and as such can be slow.
 * If the operating system runs out of entropy, key generation will block until
 * more entropy is available.
 *
 * If quick key generation is important, a hardware entropy generator, or an
 * entropy gathering daemon may be installed. For example, administrators of
 * Debian systems may want to install the 'randomsound' package.
 *
 * This class uses the experimental automated key generation support available
 * in GnuPG. See doc/DETAILS in the
 * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
 * information on the key generation format.
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Nathan Fredrickson <nathan@silverorange.com>
 * @author    Michael Gauthier <mike@silverorange.com>
 * @copyright 2005-2013 silverorange
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      http://pear.php.net/package/Crypt_GPG
 * @link      http://www.gnupg.org/
 */
class Crypt_GPG_KeyGenerator extends Crypt_GPGAbstract
{
    // {{{ protected properties

    /**
     * The expiration date of generated keys
     *
     * @var integer
     *
     * @see Crypt_GPG_KeyGenerator::setExpirationDate()
     */
    protected $expirationDate = 0;

    /**
     * The passphrase of generated keys
     *
     * @var string
     *
     * @see Crypt_GPG_KeyGenerator::setPassphrase()
     */
    protected $passphrase = '';

    /**
     * The algorithm for generated primary keys
     *
     * @var integer
     *
     * @see Crypt_GPG_KeyGenerator::setKeyParams()
     */
    protected $keyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_DSA;

    /**
     * The size of generated primary keys
     *
     * @var integer
     *
     * @see Crypt_GPG_KeyGenerator::setKeyParams()
     */
    protected $keySize = 1024;

    /**
     * The usages of generated primary keys
     *
     * This is a bitwise combination of the usage constants in
     * {@link Crypt_GPG_SubKey}.
     *
     * @var integer
     *
     * @see Crypt_GPG_KeyGenerator::setKeyParams()
     */
    protected $keyUsage = 6; // USAGE_SIGN | USAGE_CERTIFY

    /**
     * The algorithm for generated sub-keys
     *
     * @var integer
     *
     * @see Crypt_GPG_KeyGenerator::setSubKeyParams()
     */
    protected $subKeyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC;

    /**
     * The size of generated sub-keys
     *
     * @var integer
     *
     * @see Crypt_GPG_KeyGenerator::setSubKeyParams()
     */
    protected $subKeySize = 2048;

    /**
     * The usages of generated sub-keys
     *
     * This is a bitwise combination of the usage constants in
     * {@link Crypt_GPG_SubKey}.
     *
     * @var integer
     *
     * @see Crypt_GPG_KeyGenerator::setSubKeyParams()
     */
    protected $subKeyUsage = Crypt_GPG_SubKey::USAGE_ENCRYPT;

    // }}}
    // {{{ __construct()

    /**
     * Creates a new GnuPG key generator
     *
     * Available options are:
     *
     * - string  homedir        - the directory where the GPG
     *                                       keyring files are stored. If not
     *                                       specified, Crypt_GPG uses the
     *                                       default of ~/.gnupg.
     * - string  publicKeyring  - the file path of the public
     *                                       keyring. Use this if the public
     *                                       keyring is not in the homedir, or
     *                                       if the keyring is in a directory
     *                                       not writable by the process
     *                                       invoking GPG (like Apache). Then
     *                                       you can specify the path to the
     *                                       keyring with this option
     *                                       (/foo/bar/pubring.gpg), and specify
     *                                       a writable directory (like /tmp)
     *                                       using the homedir option.
     * - string  privateKeyring - the file path of the private
     *                                       keyring. Use this if the private
     *                                       keyring is not in the homedir, or
     *                                       if the keyring is in a directory
     *                                       not writable by the process
     *                                       invoking GPG (like Apache). Then
     *                                       you can specify the path to the
     *                                       keyring with this option
     *                                       (/foo/bar/secring.gpg), and specify
     *                                       a writable directory (like /tmp)
     *                                       using the homedir option.
     * - string  trustDb        - the file path of the web-of-trust
     *                                       database. Use this if the trust
     *                                       database is not in the homedir, or
     *                                       if the database is in a directory
     *                                       not writable by the process
     *                                       invoking GPG (like Apache). Then
     *                                       you can specify the path to the
     *                                       trust database with this option
     *                                       (/foo/bar/trustdb.gpg), and specify
     *                                       a writable directory (like /tmp)
     *                                       using the homedir option.
     * - string  binary         - the location of the GPG binary. If
     *                                       not specified, the driver attempts
     *                                       to auto-detect the GPG binary
     *                                       location using a list of known
     *                                       default locations for the current
     *                                       operating system. The option
     *                                       gpgBinary is a
     *                                       deprecated alias for this option.
     * - string  agent          - the location of the GnuPG agent
     *                                       binary. The gpg-agent is only
     *                                       used for GnuPG 2.x. If not
     *                                       specified, the engine attempts
     *                                       to auto-detect the gpg-agent
     *                                       binary location using a list of
     *                                       know default locations for the
     *                                       current operating system.
     * - mixed debug            - whether or not to use debug mode.
     *                                       When debug mode is on, all
     *                                       communication to and from the GPG
     *                                       subprocess is logged. This can be
     *
     * @param array $options optional. An array of options used to create the
     *                       GPG object. All options are optional and are
     *                       represented as key-value pairs.
     *
     * @throws Crypt_GPG_FileException if the homedir does not exist
     *         and cannot be created. This can happen if homedir is
     *         not specified, Crypt_GPG is run as the web user, and the web
     *         user has no home directory. This exception is also thrown if any
     *         of the options publicKeyring,
     *         privateKeyring or trustDb options are
     *         specified but the files do not exist or are are not readable.
     *         This can happen if the user running the Crypt_GPG process (for
     *         example, the Apache user) does not have permission to read the
     *         files.
     *
     * @throws PEAR_Exception if the provided binary is invalid, or
     *         if no binary is provided and no suitable binary could
     *         be found.
     *
     * @throws PEAR_Exception if the provided agent is invalid, or
     *         if no agent is provided and no suitable gpg-agent
     *         cound be found.
     */
    public function __construct(array $options = array())
    {
        parent::__construct($options);
    }

    // }}}
    // {{{ setExpirationDate()

    /**
     * Sets the expiration date of generated keys
     *
     * @param string|integer $date either a string that may be parsed by
     *                             PHP's strtotime() function, or an integer
     *                             timestamp representing the number of seconds
     *                             since the UNIX epoch. This date must be at
     *                             least one date in the future. Keys that
     *                             expire in the past may not be generated. Use
     *                             an expiration date of 0 for keys that do not
     *                             expire.
     *
     * @throws InvalidArgumentException if the date is not a valid format, or
     *                                  if the date is not at least one day in
     *                                  the future, or if the date is greater
     *                                  than 2038-01-19T03:14:07.
     *
     * @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
     */
    public function setExpirationDate($date)
    {
        if (is_int($date) || ctype_digit(strval($date))) {
            $expirationDate = intval($date);
        } else {
            $expirationDate = strtotime($date);
        }

        if ($expirationDate === false) {
            throw new InvalidArgumentException(
                sprintf(
                    'Invalid expiration date format: "%s". Please use a ' .
                    'format compatible with PHP\'s strtotime().',
                    $date
                )
            );
        }

        if ($expirationDate !== 0 && $expirationDate < time() + 86400) {
            throw new InvalidArgumentException(
                'Expiration date must be at least a day in the future.'
            );
        }

        // GnuPG suffers from the 2038 bug
        if ($expirationDate > 2147483647) {
            throw new InvalidArgumentException(
                'Expiration date must not be greater than 2038-01-19T03:14:07.'
            );
        }

        $this->expirationDate = $expirationDate;

        return $this;
    }

    // }}}
    // {{{ setPassphrase()

    /**
     * Sets the passphrase of generated keys
     *
     * @param string $passphrase the passphrase to use for generated keys. Use
     *                           null or an empty string for no passphrase.
     *
     * @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
     */
    public function setPassphrase($passphrase)
    {
        $this->passphrase = strval($passphrase);
        return $this;
    }

    // }}}
    // {{{ setKeyParams()

    /**
     * Sets the parameters for the primary key of generated key-pairs
     *
     * @param integer $algorithm the algorithm used by the key. This should be
     *                           one of the Crypt_GPG_SubKey::ALGORITHM_*
     *                           constants.
     * @param integer $size      optional. The size of the key. Different
     *                           algorithms have different size requirements.
     *                           If not specified, the default size for the
     *                           specified algorithm will be used. If an
     *                           invalid key size is used, GnuPG will do its
     *                           best to round it to a valid size.
     * @param integer $usage     optional. A bitwise combination of key usages.
     *                           If not specified, the primary key will be used
     *                           only to sign and certify. This is the default
     *                           behavior of GnuPG in interactive mode. Use
     *                           the Crypt_GPG_SubKey::USAGE_* constants here.
     *                           The primary key may be used to certify even
     *                           if the certify usage is not specified.
     *
     * @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
     */
    public function setKeyParams($algorithm, $size = 0, $usage = 0)
    {
        $algorithm = intval($algorithm);

        if ($algorithm === Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC) {
            throw new Crypt_GPG_InvalidKeyParamsException(
                'Primary key algorithm must be capable of signing. The ' .
                'Elgamal algorithm can only encrypt.',
                0,
                $algorithm,
                $size,
                $usage
            );
        }

        if ($size != 0) {
            $size = intval($size);
        }

        if ($usage != 0) {
            $usage = intval($usage);
        }

        $usageEncrypt = Crypt_GPG_SubKey::USAGE_ENCRYPT;

        if ($algorithm === Crypt_GPG_SubKey::ALGORITHM_DSA
            && ($usage & $usageEncrypt) === $usageEncrypt
        ) {
            throw new Crypt_GPG_InvalidKeyParamsException(
                'The DSA algorithm is not capable of encrypting. Please ' .
                'specify a different algorithm or do not include encryption ' .
                'as a usage for the primary key.',
                0,
                $algorithm,
                $size,
                $usage
            );
        }

        $this->keyAlgorithm = $algorithm;

        if ($size != 0) {
            $this->keySize = $size;
        }

        if ($usage != 0) {
            $this->keyUsage = $usage;
        }

        return $this;
    }

    // }}}
    // {{{ setSubKeyParams()

    /**
     * Sets the parameters for the sub-key of generated key-pairs
     *
     * @param integer $algorithm the algorithm used by the key. This should be
     *                           one of the Crypt_GPG_SubKey::ALGORITHM_*
     *                           constants.
     * @param integer $size      optional. The size of the key. Different
     *                           algorithms have different size requirements.
     *                           If not specified, the default size for the
     *                           specified algorithm will be used. If an
     *                           invalid key size is used, GnuPG will do its
     *                           best to round it to a valid size.
     * @param integer $usage     optional. A bitwise combination of key usages.
     *                           If not specified, the sub-key will be used
     *                           only to encrypt. This is the default behavior
     *                           of GnuPG in interactive mode. Use the
     *                           Crypt_GPG_SubKey::USAGE_* constants here.
     *
     * @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
     */
    public function setSubKeyParams($algorithm, $size = '', $usage = 0)
    {
        $algorithm = intval($algorithm);

        if ($size != 0) {
            $size = intval($size);
        }

        if ($usage != 0) {
            $usage = intval($usage);
        }

        $usageSign = Crypt_GPG_SubKey::USAGE_SIGN;

        if ($algorithm === Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC
            && ($usage & $usageSign) === $usageSign
        ) {
            throw new Crypt_GPG_InvalidKeyParamsException(
                'The Elgamal algorithm is not capable of signing. Please ' .
                'specify a different algorithm or do not include signing ' .
                'as a usage for the sub-key.',
                0,
                $algorithm,
                $size,
                $usage
            );
        }

        $usageEncrypt = Crypt_GPG_SubKey::USAGE_ENCRYPT;

        if ($algorithm === Crypt_GPG_SubKey::ALGORITHM_DSA
            && ($usage & $usageEncrypt) === $usageEncrypt
        ) {
            throw new Crypt_GPG_InvalidKeyParamsException(
                'The DSA algorithm is not capable of encrypting. Please ' .
                'specify a different algorithm or do not include encryption ' .
                'as a usage for the sub-key.',
                0,
                $algorithm,
                $size,
                $usage
            );
        }

        $this->subKeyAlgorithm = $algorithm;

        if ($size != 0) {
            $this->subKeySize = $size;
        }

        if ($usage != 0) {
            $this->subKeyUsage = $usage;
        }

        return $this;
    }

    // }}}
    // {{{ generateKey()

    /**
     * Generates a new key-pair in the current keyring
     *
     * Secure key generation requires true random numbers, and as such can be
     * solw. If the operating system runs out of entropy, key generation will
     * block until more entropy is available.
     *
     * If quick key generation is important, a hardware entropy generator, or
     * an entropy gathering daemon may be installed. For example,
     * administrators of Debian systems may want to install the 'randomsound'
     * package.
     *
     * @param string|Crypt_GPG_UserId $name    either a {@link Crypt_GPG_UserId}
     *                                         object, or a string containing
     *                                         the name of the user id.
     * @param string                  $email   optional. If $name is
     *                                         specified as a string, this is
     *                                         the email address of the user id.
     * @param string                  $comment optional. If $name is
     *                                         specified as a string, this is
     *                                         the comment of the user id.
     *
     * @return Crypt_GPG_Key the newly generated key.
     *
     * @throws Crypt_GPG_KeyNotCreatedException if the key parameters are
     *         incorrect, if an unknown error occurs during key generation, or
     *         if the newly generated key is not found in the keyring.
     *
     * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
     *         Use the debug option and file a bug report if these
     *         exceptions occur.
     */
    public function generateKey($name, $email = '', $comment = '')
    {
        $handle = uniqid('key', true);

        $userId = $this->getUserId($name, $email, $comment);

        $keyParams = array(
            'Key-Type'      => $this->keyAlgorithm,
            'Key-Length'    => $this->keySize,
            'Key-Usage'     => $this->getUsage($this->keyUsage),
            'Subkey-Type'   => $this->subKeyAlgorithm,
            'Subkey-Length' => $this->subKeySize,
            'Subkey-Usage'  => $this->getUsage($this->subKeyUsage),
            'Name-Real'     => $userId->getName(),
            'Handle'        => $handle,
        );

        if ($this->expirationDate != 0) {
            // GnuPG only accepts granularity of days
            $expirationDate = date('Y-m-d', $this->expirationDate);
            $keyParams['Expire-Date'] = $expirationDate;
        }

        if (strlen($this->passphrase)) {
            $keyParams['Passphrase'] = $this->passphrase;
        }

        if ($userId->getEmail() != '') {
            $keyParams['Name-Email'] = $userId->getEmail();
        }

        if ($userId->getComment() != '') {
            $keyParams['Name-Comment'] = $userId->getComment();
        }

        $keyParamsFormatted = array();
        foreach ($keyParams as $name => $value) {
            $keyParamsFormatted[] = $name . ': ' . $value;
        }

        // This is required in GnuPG 2.1
        if (!strlen($this->passphrase)) {
            $keyParamsFormatted[] = '%no-protection';
        }

        $input = implode("\n", $keyParamsFormatted) . "\n%commit\n";

        $this->engine->reset();
        $this->engine->setProcessData('Handle', $handle);
        $this->engine->setInput($input);
        $this->engine->setOutput($output);
        $this->engine->setOperation('--gen-key', array('--batch'));

        try {
            $this->engine->run();
        } catch (Crypt_GPG_InvalidKeyParamsException $e) {
            switch ($this->engine->getProcessData('LineNumber')) {
            case 1:
                throw new Crypt_GPG_InvalidKeyParamsException(
                    'Invalid primary key algorithm specified.',
                    0,
                    $this->keyAlgorithm,
                    $this->keySize,
                    $this->keyUsage
                );
            case 4:
                throw new Crypt_GPG_InvalidKeyParamsException(
                    'Invalid sub-key algorithm specified.',
                    0,
                    $this->subKeyAlgorithm,
                    $this->subKeySize,
                    $this->subKeyUsage
                );
            default:
                throw $e;
            }
        }

        $fingerprint = $this->engine->getProcessData('KeyCreated');
        $keys        = $this->_getKeys($fingerprint);

        if (count($keys) === 0) {
            throw new Crypt_GPG_KeyNotCreatedException(
                sprintf(
                    'Newly created key "%s" not found in keyring.',
                    $fingerprint
                )
            );
        }

        return $keys[0];
    }

    // }}}
    // {{{ getUsage()

    /**
     * Builds a GnuPG key usage string suitable for key generation
     *
     * See doc/DETAILS in the
     * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
     * information on the key usage format.
     *
     * @param integer $usage a bitwise combination of the key usages. This is
     *                       a combination of the Crypt_GPG_SubKey::USAGE_*
     *                       constants.
     *
     * @return string the key usage string.
     */
    protected function getUsage($usage)
    {
        $map = array(
            Crypt_GPG_SubKey::USAGE_ENCRYPT        => 'encrypt',
            Crypt_GPG_SubKey::USAGE_SIGN           => 'sign',
            Crypt_GPG_SubKey::USAGE_CERTIFY        => 'cert',
            Crypt_GPG_SubKey::USAGE_AUTHENTICATION => 'auth',
        );

        // cert is always used for primary keys and does not need to be
        // specified
        $usage &= ~Crypt_GPG_SubKey::USAGE_CERTIFY;

        $usageArray = array();

        foreach ($map as $key => $value) {
            if (($usage & $key) === $key) {
                $usageArray[] = $value;
            }
        }

        return implode(',', $usageArray);
    }

    // }}}
    // {{{ getUserId()

    /**
     * Gets a user id object from parameters
     *
     * @param string|Crypt_GPG_UserId $name    either a {@link Crypt_GPG_UserId}
     *                                         object, or a string containing
     *                                         the name of the user id.
     * @param string                  $email   optional. If $name is
     *                                         specified as a string, this is
     *                                         the email address of the user id.
     * @param string                  $comment optional. If $name is
     *                                         specified as a string, this is
     *                                         the comment of the user id.
     *
     * @return Crypt_GPG_UserId a user id object for the specified parameters.
     */
    protected function getUserId($name, $email = '', $comment = '')
    {
        if ($name instanceof Crypt_GPG_UserId) {
            $userId = $name;
        } else {
            $userId = new Crypt_GPG_UserId();
            $userId->setName($name)->setEmail($email)->setComment($comment);
        }

        return $userId;
    }

    // }}}
}

// }}}

?>


<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Contains a data class representing a GPG user id
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see
 * <http://www.gnu.org/licenses/>
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Michael Gauthier <mike@silverorange.com>
 * @copyright 2008-2010 silverorange
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      http://pear.php.net/package/Crypt_GPG
 */

// {{{ class Crypt_GPG_UserId

/**
 * A class for GPG user id information
 *
 * This class is used to store the results of the {@link Crypt_GPG::getKeys()}
 * method. User id objects are members of a {@link Crypt_GPG_Key} object.
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Michael Gauthier <mike@silverorange.com>
 * @copyright 2008-2010 silverorange
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      http://pear.php.net/package/Crypt_GPG
 * @see       Crypt_GPG::getKeys()
 * @see       Crypt_GPG_Key::getUserIds()
 */
class Crypt_GPG_UserId
{
    // {{{ class properties

    /**
     * The name field of this user id
     *
     * @var string
     */
    private $_name = '';

    /**
     * The comment field of this user id
     *
     * @var string
     */
    private $_comment = '';

    /**
     * The email field of this user id
     *
     * @var string
     */
    private $_email = '';

    /**
     * Whether or not this user id is revoked
     *
     * @var boolean
     */
    private $_isRevoked = false;

    /**
     * Whether or not this user id is valid
     *
     * @var boolean
     */
    private $_isValid = true;

    // }}}
    // {{{ __construct()

    /**
     * Creates a new user id
     *
     * User ids can be initialized from an array of named values. Available
     * names are:
     *
     * - string  name    - the name field of the user id.
     * - string  comment - the comment field of the user id.
     * - string  email   - the email field of the user id.
     * - boolean valid   - whether or not the user id is valid.
     * - boolean revoked - whether or not the user id is revoked.
     *
     * @param Crypt_GPG_UserId|string|array $userId optional. Either an
     *        existing user id object, which is copied; a user id string, which
     *        is parsed; or an array of initial values.
     */
    public function __construct($userId = null)
    {
        // parse from string
        if (is_string($userId)) {
            $userId = self::parse($userId);
        }

        // copy from object
        if ($userId instanceof Crypt_GPG_UserId) {
            $this->_name      = $userId->_name;
            $this->_comment   = $userId->_comment;
            $this->_email     = $userId->_email;
            $this->_isRevoked = $userId->_isRevoked;
            $this->_isValid   = $userId->_isValid;
        }

        // initialize from array
        if (is_array($userId)) {
            if (array_key_exists('name', $userId)) {
                $this->setName($userId['name']);
            }

            if (array_key_exists('comment', $userId)) {
                $this->setComment($userId['comment']);
            }

            if (array_key_exists('email', $userId)) {
                $this->setEmail($userId['email']);
            }

            if (array_key_exists('revoked', $userId)) {
                $this->setRevoked($userId['revoked']);
            }

            if (array_key_exists('valid', $userId)) {
                $this->setValid($userId['valid']);
            }
        }
    }

    // }}}
    // {{{ getName()

    /**
     * Gets the name field of this user id
     *
     * @return string the name field of this user id.
     */
    public function getName()
    {
        return $this->_name;
    }

    // }}}
    // {{{ getComment()

    /**
     * Gets the comments field of this user id
     *
     * @return string the comments field of this user id.
     */
    public function getComment()
    {
        return $this->_comment;
    }

    // }}}
    // {{{ getEmail()

    /**
     * Gets the email field of this user id
     *
     * @return string the email field of this user id.
     */
    public function getEmail()
    {
        return $this->_email;
    }

    // }}}
    // {{{ isRevoked()

    /**
     * Gets whether or not this user id is revoked
     *
     * @return boolean true if this user id is revoked and false if it is not.
     */
    public function isRevoked()
    {
        return $this->_isRevoked;
    }

    // }}}
    // {{{ isValid()

    /**
     * Gets whether or not this user id is valid
     *
     * @return boolean true if this user id is valid and false if it is not.
     */
    public function isValid()
    {
        return $this->_isValid;
    }

    // }}}
    // {{{ __toString()

    /**
     * Gets a string representation of this user id
     *
     * The string is formatted as:
     * name (comment) <email-address>.
     *
     * @return string a string representation of this user id.
     */
    public function __toString()
    {
        $components = array();

        if (mb_strlen($this->_name, '8bit') > 0) {
            $components[] = $this->_name;
        }

        if (mb_strlen($this->_comment, '8bit') > 0) {
            $components[] = '(' . $this->_comment . ')';
        }

        if (mb_strlen($this->_email, '8bit') > 0) {
            $components[] = '<' . $this->_email. '>';
        }

        return implode(' ', $components);
    }

    // }}}
    // {{{ setName()

    /**
     * Sets the name field of this user id
     *
     * @param string $name the name field of this user id.
     *
     * @return Crypt_GPG_UserId the current object, for fluent interface.
     */
    public function setName($name)
    {
        $this->_name = strval($name);
        return $this;
    }

    // }}}
    // {{{ setComment()

    /**
     * Sets the comment field of this user id
     *
     * @param string $comment the comment field of this user id.
     *
     * @return Crypt_GPG_UserId the current object, for fluent interface.
     */
    public function setComment($comment)
    {
        $this->_comment = strval($comment);
        return $this;
    }

    // }}}
    // {{{ setEmail()

    /**
     * Sets the email field of this user id
     *
     * @param string $email the email field of this user id.
     *
     * @return Crypt_GPG_UserId the current object, for fluent interface.
     */
    public function setEmail($email)
    {
        $this->_email = strval($email);
        return $this;
    }

    // }}}
    // {{{ setRevoked()

    /**
     * Sets whether or not this user id is revoked
     *
     * @param boolean $isRevoked whether or not this user id is revoked.
     *
     * @return Crypt_GPG_UserId the current object, for fluent interface.
     */
    public function setRevoked($isRevoked)
    {
        $this->_isRevoked = ($isRevoked) ? true : false;
        return $this;
    }

    // }}}
    // {{{ setValid()

    /**
     * Sets whether or not this user id is valid
     *
     * @param boolean $isValid whether or not this user id is valid.
     *
     * @return Crypt_GPG_UserId the current object, for fluent interface.
     */
    public function setValid($isValid)
    {
        $this->_isValid = ($isValid) ? true : false;
        return $this;
    }

    // }}}
    // {{{ parse()

    /**
     * Parses a user id object from a user id string
     *
     * A user id string is of the form:
     * name (comment) <email-address> with the comment
     * and email-address fields being optional.
     *
     * @param string $string the user id string to parse.
     *
     * @return Crypt_GPG_UserId the user id object parsed from the string.
     */
    public static function parse($string)
    {
        $userId  = new Crypt_GPG_UserId();
        $name    = '';
        $email   = '';
        $comment = '';

        // get email address from end of string if it exists
        $matches = array();
        if (preg_match('/^(.*?)<([^>]+)>$/', $string, $matches) === 1) {
            $string = trim($matches[1]);
            $email  = $matches[2];
        }

        // get comment from end of string if it exists
        $matches = array();
        if (preg_match('/^(.+?) \(([^\)]+)\)$/', $string, $matches) === 1) {
            $string  = $matches[1];
            $comment = $matches[2];
        }

        // there can be an email without a name
        if (!$email && preg_match('/^[\S]+@[\S]+$/', $string, $matches) === 1) {
            $email = $string;
        } else {
            $name = $string;
        }

        $userId->setName($name);
        $userId->setComment($comment);
        $userId->setEmail($email);

        return $userId;
    }

    // }}}
}

// }}}

?>


<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * A class for monitoring and terminating processes
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see
 * <http://www.gnu.org/licenses/>
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Michael Gauthier <mike@silverorange.com>
 * @copyright 2013 silverorange
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      http://pear.php.net/package/Crypt_GPG
 */

// {{{ class Crypt_GPG_ProcessControl

/**
 * A class for monitoring and terminating processes by PID
 *
 * This is used to safely terminate the gpg-agent for GnuPG 2.x. This class
 * is limited in its abilities and can only check if a PID is running and
 * send a PID SIGTERM.
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @author    Michael Gauthier <mike@silverorange.com>
 * @copyright 2013 silverorange
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link      http://pear.php.net/package/Crypt_GPG
 */
class Crypt_GPG_ProcessControl
{
    // {{{ protected properties

    /**
     * The PID (process identifier) being monitored
     *
     * @var integer
     */
    protected $pid;

    // }}}
    // {{{ __construct()

    /**
     * Creates a new process controller from the given PID (process identifier)
     *
     * @param integer $pid the PID (process identifier).
     */
    public function __construct($pid)
    {
        $this->pid = $pid;
    }

    // }}}
    // {{{ public function getPid()

    /**
     * Gets the PID (process identifier) being controlled
     *
     * @return integer the PID being controlled.
     */
    public function getPid()
    {
        return $this->pid;
    }

    // }}}
    // {{{ isRunning()

    /**
     * Checks if the process is running
     *
     * If the posix extension is available, posix_getpgid()
     * is used. Otherwise ps is used on UNIX-like systems and
     * tasklist on Windows.
     *
     * @return boolean true if the process is running, false if not.
     */
    public function isRunning()
    {
        $running = false;

        if (function_exists('posix_getpgid')) {
            $running = false !== posix_getpgid($this->pid);
        } elseif (PHP_OS === 'WINNT') {
            $command = 'tasklist /fo csv /nh /fi '
                . escapeshellarg('PID eq ' . $this->pid);

            $result  = exec($command);
            $parts   = explode(',', $result);
            $running = (count($parts) > 1 && trim($parts[1], '"') == $this->pid);
        } else {
            $result  = exec('ps -p ' . escapeshellarg($this->pid) . ' -o pid=');
            $running = (trim($result) == $this->pid);
        }

        return $running;
    }

    // }}}
    // {{{ terminate()

    /**
     * Ends the process gracefully
     *
     * The signal SIGTERM is sent to the process. The gpg-agent process will
     * end gracefully upon receiving the SIGTERM signal. Upon 3 consecutive
     * SIGTERM signals the gpg-agent will forcefully shut down.
     *
     * If the posix extension is available, posix_kill()
     * is used. Otherwise kill is used on UNIX-like systems and
     * taskkill is used in Windows.
     *
     * @return void
     */
    public function terminate()
    {
        if (function_exists('posix_kill')) {
            posix_kill($this->pid, 15);
        } elseif (PHP_OS === 'WINNT') {
            exec('taskkill /PID ' . escapeshellarg($this->pid));
        } else {
            exec('kill -15 ' . escapeshellarg($this->pid));
        }
    }

    // }}}
}

// }}}

?>