<?php

namespace SilverStripe\ContentReview\Reports;

use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\ContentReview\Compatibility\ContentReviewCompatability;
use SilverStripe\ContentReview\Extensions\ContentReviewOwner;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\SS_List;
use SilverStripe\Reports\Report;
use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Versioned\Versioned;

/**
 * Show all pages that need to be reviewed.
 */
class PagesDueForReviewReport extends Report
{
    /**
     * @return string
     */
    public function title()
    {
        return _t(__CLASS__ . ".TITLE", "Pages due for review");
    }

    /**
     * @return FieldList
     */
    public function parameterFields()
    {
        $filtersList = FieldList::create();

        $filtersList->push(
            DateField::create(
                "ReviewDateAfter",
                _t(__CLASS__ . ".REVIEWDATEAFTER", "Review date after or on")
            )
        );

        $filtersList->push(
            DateField::create(
                "ReviewDateBefore",
                _t(__CLASS__ . ".REVIEWDATEBEFORE", "Review date before or on"),
                date("d/m/Y", strtotime("midnight"))
            )
        );

        $filtersList->push(
            CheckboxField::create(
                "ShowVirtualPages",
                _t(__CLASS__ . ".SHOWVIRTUALPAGES", "Show Virtual Pages")
            )
        );

        $filtersList->push(
            CheckboxField::create(
                "OnlyMyPages",
                _t(__CLASS__ . ".ONLYMYPAGES", "Only Show pages assigned to me")
            )
        );

        return $filtersList;
    }

    /**
     * @return array
     */
    public function columns()
    {
        $linkBase = singleton(CMSPageEditController::class)->Link("show");
        $linkPath = parse_url($linkBase, PHP_URL_PATH);
        $linkQuery = parse_url($linkBase, PHP_URL_QUERY);

        $fields = [
            "Title" => [
                "title" => "Page name",
                "formatting" => "\$value"
            ],
            "NextReviewDate" => [
                "title" => "Review Date",
                "casting" => "Date->Full",
                "formatting" => function ($value, $item) {
                    if ($item->ContentReviewType == "Disabled") {
                        return "disabled";
                    }
                    if ($item->ContentReviewType == "Inherit") {
                        $setting = $item->getOptions();
                        if (!$setting) {
                            return "disabled";
                        }
                        return $item->obj("NextReviewDate")->Full();
                    }
                    return $value;
                }
            ],
            "OwnerNames" => [
                "title" => "Owner"
            ],
            "LastEditedByName" => "Last edited by",
            "AbsoluteLink" => [
                "title" => "URL",
                "formatting" => function ($value, $item) {
                    $liveLink = $item->AbsoluteLiveLink;
                    $stageLink = $item->AbsoluteLink();

                    return sprintf(
                        "%s %s",
                        $stageLink,
                        $liveLink ? $liveLink : $stageLink . "?stage=Stage",
                        $liveLink ? "(live)" : "(draft)"
                    );
                }
            ],
            "ContentReviewType" => [
                "title" => "Settings are",
                "formatting" => function ($value, $item) use ($linkPath, $linkQuery) {
                    if ($item->ContentReviewType == "Inherit") {
                        $options = $item->getOptions();
                        if ($options && $options instanceof SiteConfig) {
                            return "Inherited from Settings";
                        } elseif ($options) {
                            return sprintf(
                                "Inherited from %s",
                                $linkPath,
                                $options->ID,
                                $linkQuery,
                                $options->Title
                            );
                        }
                    }

                    return $value;
                }
            ],
        ];

        return $fields;
    }

    /**
     * @param array $params
     *
     * @return SS_List
     */
    public function sourceRecords($params = [])
    {
        Versioned::set_stage(Versioned::DRAFT);

        $records = SiteTree::get();
        $compatibility = ContentReviewCompatability::start();

        if (empty($params['ReviewDateBefore']) && empty($params['ReviewDateAfter'])) {
            // If there's no review dates set, default to all pages due for review now
            $records = $records->where(
                sprintf(
                    '"NextReviewDate" < \'%s\'',
                    DBDatetime::now()->Format('y-MM-dd')
                )
            );
        } else {
            // Review date before
            if (!empty($params['ReviewDateBefore'])) {
                // TODO Get value from DateField->dataValue() once we have access to form elements here
                $nextReviewUnixSec = strtotime(
                    ' + 1 day',
                    strtotime($params['ReviewDateBefore'])
                );
                $records = $records->where(
                    sprintf(
                        "\"NextReviewDate\" < '%s'",
                        DBDatetime::create()->setValue($nextReviewUnixSec)->Format('y-MM-dd')
                    )
                );
            }

            // Review date after
            if (!empty($params['ReviewDateAfter'])) {
                // TODO Get value from DateField->dataValue() once we have access to form elements here
                $records = $records->where(
                    sprintf(
                        "\"NextReviewDate\" >= '%s'",
                        DBDatetime::create()->setValue(strtotime($params['ReviewDateAfter']))->Format('y-MM-dd')
                    )
                );
            }
        }

        // Show virtual pages?
        if (empty($params["ShowVirtualPages"])) {
            $virtualPageClasses = ClassInfo::subclassesFor(VirtualPage::class);
            $records = $records->where(sprintf(
                "\"SiteTree\".\"ClassName\" NOT IN ('%s')",
                implode("','", array_values($virtualPageClasses))
            ));
        }

        // Owner dropdown
        if (!empty($params[ContentReviewOwner::class])) {
            $ownerNames = Convert::raw2sql($params[ContentReviewOwner::class]);
            $records = $records->filter("OwnerNames:PartialMatch", $ownerNames);
        }

        // Only show pages assigned to the current user?
        // This come last because it transforms $records to an ArrayList.
        if (!empty($params["OnlyMyPages"])) {
            $currentUser = Security::getCurrentUser();

            $records = $records->filterByCallback(function ($page) use ($currentUser) {
                $options = $page->getOptions();

                if ($options) {
                    foreach ($options->ContentReviewOwners() as $owner) {
                        if ($currentUser->ID == $owner->ID) {
                            return true;
                        }
                    }
                }

                return false;
            });
        }

        ContentReviewCompatability::done($compatibility);

        return $records;
    }
}


<?php

namespace SilverStripe\ContentReview\Tasks;

use Page;
use SilverStripe\ContentReview\Compatibility\ContentReviewCompatability;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Dev\BuildTask;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\Member;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer;
use SilverStripe\ContentReview\Models\ContentReviewLog;

/**
 * Daily task to send emails to the owners of content items when the review date rolls around.
 */
class ContentReviewEmails extends BuildTask
{
    /**
     * @param HTTPRequest $request
     */
    public function run($request)
    {
        $compatibility = ContentReviewCompatability::start();

        // First grab all the pages with a custom setting
        $pages = Page::get()
            ->filter('NextReviewDate:LessThanOrEqual', DBDatetime::now()->URLDate());

        $overduePages = $this->getOverduePagesForOwners($pages);

        // Lets send one email to one owner with all the pages in there instead of no of pages
        // of emails.
        foreach ($overduePages as $memberID => $pages) {
            $this->notifyOwner($memberID, $pages);
        }

        ContentReviewCompatability::done($compatibility);
    }

    /**
     * @param SS_List $pages
     *
     * @return array
     */
    protected function getOverduePagesForOwners(SS_List $pages)
    {
        $overduePages = [];

        foreach ($pages as $page) {
            if (!$page->canBeReviewedBy()) {
                continue;
            }

            // get most recent review log of current [age]
            $contentReviewLog = $page->ReviewLogs()->sort('Created DESC')->first();

            // check log date vs NextReviewDate. If someone has left a content review
            // after the review date, then we don't need to notify anybody
            if ($contentReviewLog && $contentReviewLog->Created >= $page->NextReviewDate) {
                $page->advanceReviewDate();
                continue;
            }

            $options = $page->getOptions();

            if ($options) {
                foreach ($options->ContentReviewOwners() as $owner) {
                    if (!isset($overduePages[$owner->ID])) {
                        $overduePages[$owner->ID] = ArrayList::create();
                    }

                    $overduePages[$owner->ID]->push($page);
                }
            }
        }

        return $overduePages;
    }

    /**
     * @param int           $ownerID
     * @param array|SS_List $pages
     */
    protected function notifyOwner($ownerID, SS_List $pages)
    {
        // Prepare variables
        $siteConfig = SiteConfig::current_site_config();
        $owner = Member::get()->byID($ownerID);
        $templateVariables = $this->getTemplateVariables($owner, $siteConfig, $pages);

        // Build email
        $email = Email::create();
        $email->setTo($owner->Email);
        $email->setFrom($siteConfig->ReviewFrom);
        $email->setSubject($siteConfig->ReviewSubject);

        // Get user-editable body
        $body = $this->getEmailBody($siteConfig, $templateVariables);

        // Populate mail body with fixed template
        $email->setHTMLTemplate($siteConfig->config()->get('content_review_template'));
        $email->setData(
            array_merge(
                $templateVariables,
                [
                    'EmailBody' => $body,
                    'Recipient' => $owner,
                    'Pages' => $pages,
                ]
            )
        );
        $email->send();
    }

    /**
     * Get string value of HTML body with all variable evaluated.
     *
     * @param SiteConfig $config
     * @param array List of safe template variables to expose to this template
     *
     * @return HTMLText
     */
    protected function getEmailBody($config, $variables)
    {
        $template = SSViewer::fromString($config->ReviewBody);
        $value = $template->process(ArrayData::create($variables));

        // Cast to HTML
        return DBField::create_field('HTMLText', (string) $value);
    }

    /**
     * Gets list of safe template variables and their values which can be used
     * in both the static and editable templates.
     *
     * {@see ContentReviewAdminHelp.ss}
     *
     * @param Member     $recipient
     * @param SiteConfig $config
     * @param SS_List    $pages
     *
     * @return array
     */
    protected function getTemplateVariables($recipient, $config, $pages)
    {
        return [
            'Subject' => $config->ReviewSubject,
            'PagesCount' => $pages->count(),
            'FromEmail' => $config->ReviewFrom,
            'ToFirstName' => $recipient->FirstName,
            'ToSurname' => $recipient->Surname,
            'ToEmail' => $recipient->Email,
        ];
    }
}


<?php

namespace SilverStripe\ContentReview\Extensions;

use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Email\Email;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Security\Group;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;

/**
 * This extensions add a default schema for new pages and pages without a content
 * review setting.
 *
 * @property int $ReviewPeriodDays
 */
class ContentReviewDefaultSettings extends DataExtension
{
    /**
     * @config
     *
     * @var array
     */
    private static $db = array(
        'ReviewPeriodDays' => 'Int',
        'ReviewFrom' => 'Varchar(255)',
        'ReviewSubject' => 'Varchar(255)',
        'ReviewBody' => 'HTMLText',
    );

    /**
     * @config
     *
     * @var array
     */
    private static $defaults = array(
        'ReviewSubject' => 'Page(s) are due for content review',
        'ReviewBody' => '
Page(s) due for review
'
            . '
There are $PagesCount pages that are due for review today by you.
',
    );

    /**
     * @config
     *
     * @var array
     */
    private static $many_many = [
        'ContentReviewGroups' => Group::class,
        'ContentReviewUsers' => Member::class,
    ];

    /**
     * Template to use for content review emails.
     *
     * This should contain an $EmailBody variable as a placeholder for the user-defined copy
     *
     * @config
     *
     * @var string
     */
    private static $content_review_template = 'SilverStripe\\ContentReview\\ContentReviewEmail';

    /**
     * @return string
     */
    public function getOwnerNames()
    {
        $names = [];

        foreach ($this->OwnerGroups() as $group) {
            $names[] = $group->getBreadcrumbs(' > ');
        }

        foreach ($this->OwnerUsers() as $group) {
            $names[] = $group->getName();
        }

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

    /**
     * @return ManyManyList
     */
    public function OwnerGroups()
    {
        return $this->owner->getManyManyComponents('ContentReviewGroups');
    }

    /**
     * @return ManyManyList
     */
    public function OwnerUsers()
    {
        return $this->owner->getManyManyComponents('ContentReviewUsers');
    }

    /**
     * @param FieldList $fields
     */
    public function updateCMSFields(FieldList $fields)
    {
        $helpText = LiteralField::create(
            'ContentReviewHelp',
            _t(
                __CLASS__ . '.DEFAULTSETTINGSHELP',
                'These settings will apply to all pages that do not have a specific Content Review schedule.'
            )
        );

        $fields->addFieldToTab('Root.ContentReview', $helpText);

        $reviewFrequency = DropdownField::create(
            'ReviewPeriodDays',
            _t(__CLASS__ . '.REVIEWFREQUENCY', 'Review frequency'),
            SiteTreeContentReview::get_schedule()
        )
            ->setDescription(_t(
                __CLASS__ . '.REVIEWFREQUENCYDESCRIPTION',
                'The review date will be set to this far in the future, whenever the page is published.'
            ));

        $fields->addFieldToTab('Root.ContentReview', $reviewFrequency);

        $users = Permission::get_members_by_permission([
            'CMS_ACCESS_CMSMain',
            'ADMIN',
        ]);

        $usersMap = $users->map('ID', 'Title')->toArray();
        asort($usersMap);

        $userField = ListboxField::create('OwnerUsers', _t(__CLASS__ . '.PAGEOWNERUSERS', 'Users'), $usersMap)
            ->setAttribute('data-placeholder', _t(__CLASS__ . '.ADDUSERS', 'Add users'))
            ->setDescription(_t(__CLASS__ . '.OWNERUSERSDESCRIPTION', 'Page owners that are responsible for reviews'));

        $fields->addFieldToTab('Root.ContentReview', $userField);

        $groupsMap = [];

        foreach (Group::get() as $group) {
            // Listboxfield values are escaped, use ASCII char instead of »
            $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
        }

        asort($groupsMap);

        $groupField = ListboxField::create('OwnerGroups', _t(__CLASS__ . '.PAGEOWNERGROUPS', 'Groups'), $groupsMap)
            ->setAttribute('data-placeholder', _t(__CLASS__ . '.ADDGROUP', 'Add groups'))
            ->setDescription(_t(__CLASS__ . '.OWNERGROUPSDESCRIPTION', 'Page owners that are responsible for reviews'));

        $fields->addFieldToTab('Root.ContentReview', $groupField);

        // Email content
        $fields->addFieldsToTab(
            'Root.ContentReview',
            [
                TextField::create('ReviewFrom', _t(__CLASS__ . '.EMAILFROM', 'From email address'))
                    ->setDescription(_t(__CLASS__ . '.EMAILFROM_RIGHTTITLE', 'e.g: do-not-reply@site.com')),
                TextField::create('ReviewSubject', _t(__CLASS__ . '.EMAILSUBJECT', 'Subject line')),
                TextAreaField::create('ReviewBody', _t(__CLASS__ . '.EMAILTEMPLATE', 'Email template')),
                LiteralField::create(
                    'TemplateHelp',
                    $this->owner->renderWith('SilverStripe\\ContentReview\\ContentReviewAdminHelp')
                ),
            ]
        );
    }

    /**
     * Get all Members that are default Content Owners. This includes checking group hierarchy
     * and adding any direct users.
     *
     * @return ArrayList
     */
    public function ContentReviewOwners()
    {
        return SiteTreeContentReview::merge_owners($this->OwnerGroups(), $this->OwnerUsers());
    }

    /**
     * Get the review body, falling back to the default if left blank.
     *
     * @return string HTML text
     */
    public function getReviewBody()
    {
        return $this->getWithDefault('ReviewBody');
    }

    /**
     * Get the review subject line, falling back to the default if left blank.
     *
     * @return string plain text value
     */
    public function getReviewSubject()
    {
        return $this->getWithDefault('ReviewSubject');
    }

    /**
     * Get the "from" field for review emails.
     *
     * @return string
     */
    public function getReviewFrom()
    {
        $from = $this->owner->getField('ReviewFrom');
        if ($from) {
            return $from;
        }

        // Fall back to admin email
        return Config::inst()->get(Email::class, 'admin_email');
    }

    /**
     * Get the value of a user-configured field, falling back to the default if left blank.
     *
     * @param string $field
     *
     * @return string
     */
    protected function getWithDefault($field)
    {
        $value = $this->owner->getField($field);
        if ($value) {
            return $value;
        }
        // fallback to default value
        $defaults = $this->owner->config()->get('defaults');
        if (isset($defaults[$field])) {
            return $defaults[$field];
        }
    }
}


<?php

namespace SilverStripe\ContentReview\Reports;

use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\ContentReview\Compatibility\ContentReviewCompatability;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\Reports\Report;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Versioned\Versioned;

/**
 * Show all pages that need to be reviewed.
 */
class PagesWithoutReviewScheduleReport extends Report
{
    /**
     * @return string
     */
    public function title()
    {
        return _t(__CLASS__ . ".TITLE", "Pages without a scheduled review.");
    }

    /**
     * @return FieldList
     */
    public function parameterFields()
    {
        $params = FieldList::create();
        $params->push(CheckboxField::create("ShowVirtualPages", "Show Virtual Pages"));

        return $params;
    }

    /**
     * @return array
     */
    public function columns()
    {
        $linkBase = singleton(CMSPageEditController::class)->Link("show");
        $linkPath = parse_url($linkBase, PHP_URL_PATH);
        $linkQuery = parse_url($linkBase, PHP_URL_QUERY);

        $fields = [
            "Title"             => [
                "title"      => "Page name",
                "formatting" => "\$value",
            ],
            "NextReviewDate"    => [
                "title"   => "Review Date",
                "casting" => "Date->Full",
            ],
            "OwnerNames"        => [
                "title" => "Owner",
            ],
            "LastEditedByName"  => "Last edited by",
            "AbsoluteLink"      => [
                "title"      => "URL",
                "formatting" => function ($value, $item) {
                    $liveLink = $item->AbsoluteLiveLink;
                    $stageLink = $item->AbsoluteLink();

                    return sprintf(
                        "%s %s",
                        $stageLink,
                        $liveLink ? $liveLink : $stageLink . "?stage=Stage",
                        $liveLink ? "(live)" : "(draft)"
                    );
                },
            ],
            "ContentReviewType" => [
                "title"      => "Settings are",
                "formatting" => function ($value, $item) use ($linkPath, $linkQuery) {
                    if ($item->ContentReviewType == "Inherit") {
                        $options = $item->getOptions();
                        if ($options && $options instanceof SiteConfig) {
                            return "Inherited from Settings";
                        } elseif ($options) {
                            return sprintf(
                                "Inherited from %s",
                                $linkPath,
                                $options->ID,
                                $linkQuery,
                                $options->Title
                            );
                        }
                    }

                    return $value;
                },
            ],
        ];

        return $fields;
    }

    /**
     * @param array $params
     *
     * @return SS_List
     */
    public function sourceRecords($params = [])
    {
        Versioned::set_stage(Versioned::DRAFT);

        $records = SiteTree::get();
        $compatibility = ContentReviewCompatability::start();

        // If there's no review dates set, default to all pages due for review now.

        // Show virtual pages?
        if (empty($params["ShowVirtualPages"])) {
            $virtualPageClasses = ClassInfo::subclassesFor(VirtualPage::class);
            $records = $records->where(sprintf(
                "\"SiteTree\".\"ClassName\" NOT IN ('%s')",
                implode("','", array_values($virtualPageClasses))
            ));
        }

        $records->sort("ParentID");
        $records = $records->toArray();

        // Trim out calculated values
        $list = ArrayList::create();
        foreach ($records as $record) {
            if (!$this->hasReviewSchedule($record)) {
                $list->push($record);
            }
        }

        ContentReviewCompatability::done($compatibility);

        return $list;
    }

    /**
     * @param DataObject $record
     *
     * @return bool
     */
    protected function hasReviewSchedule(DataObject $record)
    {
        if (!$record->obj("NextReviewDate")->exists()) {
            return false;
        }

        $options = $record->getOptions();

        if ($options && $options->OwnerGroups()->count() == 0 && $options->OwnerUsers()->count() == 0) {
            return false;
        }

        return true;
    }
}


<?php

namespace SilverStripe\ContentReview\Compatibility;

use SilverStripe\Core\ClassInfo;
use SilverStripe\Subsites\Model\Subsite;

/**
 * This is a helper class which lets us do things with content review data without subsites
 * messing our SQL queries up.
 *
 * Make sure any DataQuery instances you are building are BOTH created & executed between start()
 * and done() because augmentDataQueryCreate and augmentSQL happens there.
 */
class ContentReviewCompatability
{
    const SUBSITES = 0;

    /**
     * Returns the state of other modules before compatibility mode is started.
     *
     * @return array
     */
    public static function start()
    {
        $compatibility = [
            self::SUBSITES => null,
        ];

        if (ClassInfo::exists(Subsite::class)) {
            $compatibility[self::SUBSITES] = Subsite::$disable_subsite_filter;
            Subsite::disable_subsite_filter(true);
        }

        return $compatibility;
    }

    /**
     * @param array $compatibility
     */
    public static function done(array $compatibility)
    {
        if (class_exists(Subsite::class)) {
            Subsite::$disable_subsite_filter = $compatibility[self::SUBSITES];
        }
    }
}


<?php

namespace SilverStripe\ContentReview\Jobs;

use SilverStripe\ContentReview\Tasks\ContentReviewEmails;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Config;
use Symbiote\QueuedJobs\Services\AbstractQueuedJob;
use Symbiote\QueuedJobs\Services\QueuedJob;
use Symbiote\QueuedJobs\Services\QueuedJobService;

if (!class_exists(AbstractQueuedJob::class)) {
    return;
}

/**
 * Allows the content review module to use the optional queued jobs module to automatically
 * process content review emails. If the module isn't installed, nothing is done - SilverStripe
 * will never include this class declaration.
 *
 * If the module is installed, it will create a new job to be processed once every day by default.
 *
 * @see https://github.com/silverstripe-australia/silverstripe-queuedjobs
 */
class ContentReviewNotificationJob extends AbstractQueuedJob implements QueuedJob
{
    /**
     * The hour that the first job will be created at (for the next day). All other jobs should
     * be triggered around this time too, as the next generation is queued when this job is run.
     *
     * @var int
     *
     * @config
     */
    private static $first_run_hour = 9;

    /**
     * The hour at which to run these jobs.
     *
     * @var int
     *
     * @config
     */
    private static $next_run_hour = 9;

    /**
     * The minutes past the hour (see above) at which to run these jobs.
     *
     * @var int
     *
     * @config
     */
    private static $next_run_minute = 0;

    /**
     * The number of days to skip between job runs (1 means run this job every day,
     * 2 means run it every second day etc).
     *
     * @var int
     *
     * @config
     */
    private static $next_run_in_days = 1;

    /**
     * @return string
     */
    public function getTitle()
    {
        return "Content Review Notification Job";
    }

    /**
     * @return string
     */
    public function getJobType()
    {
        $this->totalSteps = 1;

        return QueuedJob::QUEUED;
    }

    public function setup()
    {
        parent::setup();

        // Recommended for long running jobs that don't increment 'currentStep'
        // https://github.com/silverstripe-australia/silverstripe-queuedjobs
        $this->currentStep = -1;
    }

    public function process()
    {
        $this->queueNextRun();

        $task = new ContentReviewEmails();
        $task->run(new HTTPRequest("GET", "/dev/tasks/ContentReviewEmails"));

        $this->currentStep = 1;
        $this->isComplete = true;
    }

    /**
     * Queue up the next job to run.
     */
    protected function queueNextRun()
    {
        $nextRun = new ContentReviewNotificationJob();

        $nextRunTime = mktime(
            Config::inst()->get(__CLASS__, 'next_run_hour'),
            Config::inst()->get(__CLASS__, 'next_run_minute'),
            0,
            date("m"),
            date("d") + Config::inst()->get(__CLASS__, 'next_run_in_days'),
            date("Y")
        );

        singleton(QueuedJobService::class)->queueJob(
            $nextRun,
            date("Y-m-d H:i:s", $nextRunTime)
        );
    }
}


<?php

namespace SilverStripe\ContentReview\Extensions;

use Exception;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\ContentReview\Jobs\ContentReviewNotificationJob;
use SilverStripe\ContentReview\Models\ContentReviewLog;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\DateTimeField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\ReadonlyField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDate;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\Group;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\PermissionProvider;
use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\View\Requirements;
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor;
use Symbiote\QueuedJobs\Services\QueuedJobService;

/**
 * Set dates at which content needs to be reviewed and provide a report and emails to alert
 * to content needing review.
 *
 * @property string $ContentReviewType
 * @property int $ReviewPeriodDays
 * @property Date $NextReviewDate
 * @property string $LastEditedByName
 * @property string $OwnerNames
 *
 * @method DataList ReviewLogs()
 * @method DataList ContentReviewGroups()
 * @method DataList ContentReviewUsers()
 */
class SiteTreeContentReview extends DataExtension implements PermissionProvider
{
    /**
     * @var array
     */
    private static $db = [
        "ContentReviewType" => "Enum('Inherit, Disabled, Custom', 'Inherit')",
        "ReviewPeriodDays" => "Int",
        "NextReviewDate" => "Date",
        "LastEditedByName" => "Varchar(255)",
        "OwnerNames" => "Varchar(255)",
    ];

    /**
     * @var array
     */
    private static $defaults = [
        "ContentReviewType" => "Inherit",
    ];

    /**
     * @var array
     */
    private static $has_many = [
        "ReviewLogs" => ContentReviewLog::class,
    ];

    /**
     * @var array
     */
    private static $belongs_many_many = [
        "ContentReviewGroups" => Group::class,
        "ContentReviewUsers" => Member::class,
    ];

    /**
     * @var array
     */
    private static $schedule = [
        0 => "No automatic review date",
        1 => "1 day",
        7 => "1 week",
        30 => "1 month",
        60 => "2 months",
        91 => "3 months",
        121 => "4 months",
        152 => "5 months",
        183 => "6 months",
        365 => "12 months",
    ];

    /**
     * @return array
     */
    public static function get_schedule()
    {
        return Config::inst()->get(static::class, 'schedule');
    }

    /**
     * Takes a list of groups and members and return a list of unique member.
     *
     * @param SS_List $groups
     * @param SS_List $members
     *
     * @return ArrayList
     */
    public static function merge_owners(SS_List $groups, SS_List $members)
    {
        $contentReviewOwners = new ArrayList();

        if ($groups->count()) {
            $groupIDs = [];

            foreach ($groups as $group) {
                $familyIDs = $group->collateFamilyIDs();

                if (is_array($familyIDs)) {
                    $groupIDs = array_merge($groupIDs, array_values($familyIDs));
                }
            }

            array_unique($groupIDs);

            if (count($groupIDs)) {
                $groupMembers = DataObject::get(Member::class)
                    ->where("\"Group\".\"ID\" IN (" . implode(",", $groupIDs) . ")")
                    ->leftJoin("Group_Members", "\"Member\".\"ID\" = \"Group_Members\".\"MemberID\"")
                    /** @skipUpgrade */
                    ->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"");

                $contentReviewOwners->merge($groupMembers);
            }
        }

        $contentReviewOwners->merge($members);
        $contentReviewOwners->removeDuplicates();

        return $contentReviewOwners;
    }

    /**
     * @param FieldList $actions
     */
    public function updateCMSActions(FieldList $actions)
    {
        if (!$this->canBeReviewedBy(Security::getCurrentUser())) {
            return;
        }

        Requirements::css('silverstripe/contentreview:client/dist/styles/contentreview.css');
        Requirements::javascript('silverstripe/contentreview:client/dist/js/contentreview.js');

        $reviewTab = LiteralField::create('ContentReviewButton', $this->owner->renderWith(__CLASS__ . '_button'));
        $actions->insertAfter('MajorActions', $reviewTab);
    }

    /**
     * Returns false if the content review have disabled.
     *
     * @param SiteTree $page
     *
     * @return bool|DBDate
     */
    public function getReviewDate(SiteTree $page = null)
    {
        if ($page === null) {
            $page = $this->owner;
        }

        if ($page->obj('NextReviewDate')->exists()) {
            return $page->obj('NextReviewDate');
        }

        $options = $this->owner->getOptions();

        if (!$options) {
            return false;
        }

        if (!$options->ReviewPeriodDays) {
            return false;
        }

        // Failover to check on ReviewPeriodDays + LastEdited
        $nextReviewUnixSec = strtotime(' + ' . $options->ReviewPeriodDays . ' days', DBDatetime::now()->getTimestamp());
        $date = DBDate::create('NextReviewDate');
        $date->setValue($nextReviewUnixSec);
        return $date;
    }

    /**
     * Get the object that have the information about the content review settings. Either:
     *
     *  - a SiteTreeContentReview decorated object
     *  - the default SiteTree config
     *  - false if this page have it's content review disabled
     *
     * Will go through parents and root pages will use the site config if their setting is Inherit.
     *
     * @return bool|DataObject
     *
     * @throws Exception
     */
    public function getOptions()
    {
        if ($this->owner->ContentReviewType == "Custom") {
            return $this->owner;
        }

        if ($this->owner->ContentReviewType == "Disabled") {
            return false;
        }

        $page = $this->owner;

        // $page is inheriting it's settings from it's parent, find
        // the first valid parent with a valid setting
        while ($parent = $page->Parent()) {
            // Root page, use site config
            if (!$parent->exists()) {
                return SiteConfig::current_site_config();
            }

            if ($parent->ContentReviewType == "Custom") {
                return $parent;
            }

            if ($parent->ContentReviewType == "Disabled") {
                return false;
            }

            $page = $parent;
        }

        throw new Exception("This shouldn't really happen, as per usual developer logic.");
    }

    /**
     * @return string
     */
    public function getOwnerNames()
    {
        $options = $this->getOptions();

        $names = [];

        if (!$options) {
            return "";
        }

        foreach ($options->OwnerGroups() as $group) {
            $names[] = $group->getBreadcrumbs(" > ");
        }

        foreach ($options->OwnerUsers() as $group) {
            $names[] = $group->getName();
        }

        return implode(", ", $names);
    }

    /**
     * @return null|string
     */
    public function getEditorName()
    {
        $member = Security::getCurrentUser();

        if ($member) {
            return $member->getTitle();
        }

        return null;
    }

    /**
     * Get all Members that are Content Owners to this page. This includes checking group
     * hierarchy and adding any direct users.
     *
     * @return ArrayList
     */
    public function ContentReviewOwners()
    {
        return SiteTreeContentReview::merge_owners(
            $this->OwnerGroups(),
            $this->OwnerUsers()
        );
    }

    /**
     * @return ManyManyList
     */
    public function OwnerGroups()
    {
        return $this->owner->getManyManyComponents("ContentReviewGroups");
    }

    /**
     * @return ManyManyList
     */
    public function OwnerUsers()
    {
        return $this->owner->getManyManyComponents("ContentReviewUsers");
    }

    /**
     * @param FieldList $fields
     */
    public function updateSettingsFields(FieldList $fields)
    {
        if ($this->owner->hasMethod('displayContentReview') && !$this->owner->displayContentReview()) {
            return;
        }

        Requirements::javascript('silverstripe/contentreview:client/dist/js/contentreview.js');
        // Display read-only version only
        if (!Permission::check("EDIT_CONTENT_REVIEW_FIELDS")) {
            $schedule = self::get_schedule();
            $contentOwners = ReadonlyField::create(
                "ROContentOwners",
                _t(__CLASS__ . ".CONTENTOWNERS", "Content Owners"),
                $this->getOwnerNames()
            );
            $nextReviewAt = DateField::create(
                'RONextReviewDate',
                _t(__CLASS__ . ".NEXTREVIEWDATE", "Next review date"),
                $this->owner->NextReviewDate
            );

            if (!isset($schedule[$this->owner->ReviewPeriodDays])) {
                $reviewFreq = ReadonlyField::create(
                    "ROReviewPeriodDays",
                    _t(__CLASS__ . ".REVIEWFREQUENCY", "Review frequency"),
                    $schedule[0]
                );
            } else {
                $reviewFreq = ReadonlyField::create(
                    "ROReviewPeriodDays",
                    _t(__CLASS__ . ".REVIEWFREQUENCY", "Review frequency"),
                    $schedule[$this->owner->ReviewPeriodDays]
                );
            }

            $logConfig = GridFieldConfig::create()
                ->addComponent(Injector::inst()->create(GridFieldSortableHeader::class))
                ->addComponent($logColumns = Injector::inst()->create(GridFieldDataColumns::class));

            // Cast the value to the users preferred date format
            $logColumns->setFieldCasting([
                'Created' => DateTimeField::class . '->value',
            ]);

            $logs = GridField::create("ROReviewNotes", "Review Notes", $this->owner->ReviewLogs(), $logConfig);

            $optionsFrom = ReadonlyField::create(
                "ROType",
                _t(__CLASS__ . ".SETTINGSFROM", "Options are"),
                $this->owner->ContentReviewType
            );

            $fields->addFieldsToTab("Root.ContentReview", [
                $contentOwners,
                $nextReviewAt->performReadonlyTransformation(),
                $reviewFreq,
                $optionsFrom,
                $logs,
            ]);

            return;
        }

        $options = [];
        $options["Disabled"] = _t(__CLASS__ . ".DISABLE", "Disable content review");
        $options["Inherit"] = _t(__CLASS__ . ".INHERIT", "Inherit from parent page");
        $options["Custom"] = _t(__CLASS__ . ".CUSTOM", "Custom settings");

        $viewersOptionsField = OptionsetField::create(
            "ContentReviewType",
            _t(__CLASS__ . ".OPTIONS", "Options"),
            $options
        );

        $users = Permission::get_members_by_permission(["CMS_ACCESS_CMSMain", "ADMIN"]);

        $usersMap = $users->map("ID", "Title")->toArray();

        asort($usersMap);

        $userField = ListboxField::create("OwnerUsers", _t(__CLASS__ . ".PAGEOWNERUSERS", "Users"), $usersMap)
            ->addExtraClass('custom-setting')
            ->setAttribute("data-placeholder", _t(__CLASS__ . ".ADDUSERS", "Add users"))
            ->setDescription(_t(__CLASS__ . '.OWNERUSERSDESCRIPTION', 'Page owners that are responsible for reviews'));

        $groupsMap = [];

        foreach (Group::get() as $group) {
            $groupsMap[$group->ID] = $group->getBreadcrumbs(" > ");
        }
        asort($groupsMap);

        $groupField = ListboxField::create("OwnerGroups", _t(__CLASS__ . ".PAGEOWNERGROUPS", "Groups"), $groupsMap)
            ->addExtraClass('custom-setting')
            ->setAttribute("data-placeholder", _t(__CLASS__ . ".ADDGROUP", "Add groups"))
            ->setDescription(_t(__CLASS__ . ".OWNERGROUPSDESCRIPTION", "Page owners that are responsible for reviews"));

        $reviewDate = DateField::create("NextReviewDate", _t(__CLASS__ . ".NEXTREVIEWDATE", "Next review date"))
            ->setDescription(_t(__CLASS__ . ".NEXTREVIEWDATADESCRIPTION", "Leave blank for no review"));

        $reviewFrequency = DropdownField::create(
            "ReviewPeriodDays",
            _t(__CLASS__ . ".REVIEWFREQUENCY", "Review frequency"),
            self::get_schedule()
        )
            ->addExtraClass('custom-setting')
            ->setDescription(_t(
                __CLASS__ . ".REVIEWFREQUENCYDESCRIPTION",
                "The review date will be set to this far in the future whenever the page is published"
            ));

        $notesField = GridField::create(
            "ReviewNotes",
            "Review Notes",
            $this->owner->ReviewLogs(),
            GridFieldConfig_RecordEditor::create()
        );

        $fields->addFieldsToTab("Root.ContentReview", [
            HeaderField::create('ContentReviewHeader', _t(__CLASS__ . ".REVIEWHEADER", "Content review"), 2),
            $viewersOptionsField,
            CompositeField::create(
                $userField,
                $groupField,
                $reviewDate,
                $reviewFrequency
            )->addExtraClass("review-settings"),
            ReadonlyField::create(
                "ROContentOwners",
                _t(__CLASS__ . ".CONTENTOWNERS", "Content Owners"),
                $this->getOwnerNames()
            ),
            $notesField,
        ]);
    }

    /**
     * Creates a ContentReviewLog and connects it to this Page.
     *
     * @param Member $reviewer
     * @param string $message
     */
    public function addReviewNote(Member $reviewer, $message)
    {
        $reviewLog = ContentReviewLog::create();
        $reviewLog->Note = $message;
        $reviewLog->ReviewerID = $reviewer->ID;
        $this->owner->ReviewLogs()->add($reviewLog);
    }

    /**
     * Advance review date to the next date based on review period or set it to null
     * if there is no schedule. Returns true if date was required and false is content
     * review is 'off'.
     *
     * @return bool
     */
    public function advanceReviewDate()
    {
        $nextDateTimestamp = false;
        $options = $this->getOptions();

        if ($options && $options->ReviewPeriodDays) {
            $nextDateTimestamp = strtotime(
                ' + ' . $options->ReviewPeriodDays . ' days',
                DBDatetime::now()->getTimestamp()
            );

            $this->owner->NextReviewDate = DBDate::create()->setValue($nextDateTimestamp)->Format(DBDate::ISO_DATE);
            $this->owner->write();
        }

        return (bool)$nextDateTimestamp;
    }

    /**
     * Check if a review is due by a member for this owner.
     *
     * @param Member $member
     *
     * @return bool
     */
    public function canBeReviewedBy(Member $member = null)
    {
        if (!$this->owner->obj("NextReviewDate")->exists()) {
            return false;
        }

        if ($this->owner->obj("NextReviewDate")->InFuture()) {
            return false;
        }

        $options = $this->getOptions();

        if (!$options) {
            return false;
        }

        if (!$options
            // Options can be a SiteConfig with different extension applied
            || (!$options->hasExtension(__CLASS__)
                && !$options->hasExtension(ContentReviewDefaultSettings::class))
        ) {
            return false;
        }

        if ($options->OwnerGroups()->count() == 0 && $options->OwnerUsers()->count() == 0) {
            return false;
        }

        if (!$member) {
            return true;
        }

        if ($member->inGroups($options->OwnerGroups())) {
            return true;
        }

        if ($options->OwnerUsers()->find("ID", $member->ID)) {
            return true;
        }

        return false;
    }

    /**
     * Set the review data from the review period, if set.
     */
    public function onBeforeWrite()
    {
        // Only update if DB fields have been changed
        $changedFields = $this->owner->getChangedFields(true, 2);
        if ($changedFields) {
            $this->owner->LastEditedByName = $this->owner->getEditorName();
            $this->owner->OwnerNames = $this->owner->getOwnerNames();
        }

        // If the user changed the type, we need to recalculate the review date.
        if ($this->owner->isChanged("ContentReviewType", 2)) {
            if ($this->owner->ContentReviewType == "Disabled") {
                $this->setDefaultReviewDateForDisabled();
            } elseif ($this->owner->ContentReviewType == "Custom") {
                $this->setDefaultReviewDateForCustom();
            } else {
                $this->setDefaultReviewDateForInherited();
            }
        }

        // Ensure that a inherited page always have a next review date
        if ($this->owner->ContentReviewType == "Inherit" && !$this->owner->NextReviewDate) {
            $this->setDefaultReviewDateForInherited();
        }

        // We need to update all the child pages that inherit this setting. We can only
        // change children after this record has been created, otherwise the stageChildren
        // method will grab all pages in the DB (this messes up unit testing)
        if (!$this->owner->exists()) {
            return;
        }

        // parent page change its review period
        // && !$this->owner->isChanged('ContentReviewType', 2)
        if ($this->owner->isChanged('ReviewPeriodDays', 2)) {
            $nextReviewUnixSec = strtotime(
                ' + ' . $this->owner->ReviewPeriodDays . ' days',
                DBDatetime::now()->getTimestamp()
            );
            $this->owner->NextReviewDate = DBDate::create()->setValue($nextReviewUnixSec)->Format('y-MM-dd');
        }
    }

    private function setDefaultReviewDateForDisabled()
    {
        $this->owner->NextReviewDate = null;
    }

    protected function setDefaultReviewDateForCustom()
    {
        // Don't overwrite existing value
        if ($this->owner->NextReviewDate) {
            return;
        }

        $this->owner->NextReviewDate = null;
        $nextDate = $this->getReviewDate();

        if (is_object($nextDate)) {
            $this->owner->NextReviewDate = $nextDate->getValue();
        } else {
            $this->owner->NextReviewDate = $nextDate;
        }
    }

    protected function setDefaultReviewDateForInherited()
    {
        // Don't overwrite existing value
        if ($this->owner->NextReviewDate) {
            return;
        }

        $options = $this->getOptions();
        $nextDate = null;

        if ($options instanceof SiteTree) {
            $nextDate = $this->getReviewDate($options);
        } elseif ($options instanceof SiteConfig) {
            $nextDate = $this->getReviewDate();
        }

        if (is_object($nextDate)) {
            $this->owner->NextReviewDate = $nextDate->getValue();
        } else {
            $this->owner->NextReviewDate = $nextDate;
        }
    }

    /**
     * Provide permissions to the CMS.
     *
     * @return array
     */
    public function providePermissions()
    {
        return [
            "EDIT_CONTENT_REVIEW_FIELDS" => [
                "name"     => "Set content owners and review dates",
                "category" => _t("SilverStripe\\Security\\Permission.CONTENT_CATEGORY", "Content permissions"),
                "sort"     => 50,
            ],
        ];
    }

    /**
     * If the queued jobs module is installed, queue up the first job for 9am tomorrow morning
     * (by default).
     */
    public function requireDefaultRecords()
    {
        if (class_exists(ContentReviewNotificationJob::class)) {
            // Ensure there is not already a job queued
            if (QueuedJobDescriptor::get()->filter("Implementation", ContentReviewNotificationJob::class)->first()) {
                return;
            }

            $nextRun = Injector::inst()->create(ContentReviewNotificationJob::class);
            $runHour = Config::inst()->get(ContentReviewNotificationJob::class, "first_run_hour");
            $firstRunTime = date(
                "Y-m-d H:i:s",
                mktime($runHour, 0, 0, date("m"), date("d") + 1, date("y"))
            );

            singleton(QueuedJobService::class)->queueJob(
                $nextRun,
                $firstRunTime
            );

            DB::alteration_message(sprintf("Added ContentReviewNotificationJob to run at %s", $firstRunTime));
        }
    }
}


<?php

namespace SilverStripe\ContentReview\Forms;

use SilverStripe\ContentReview\Extensions\SiteTreeContentReview;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Security;

class ReviewContentHandler
{
    use Injectable;

    /**
     * Parent controller for this form
     *
     * @var Controller
     */
    protected $controller;

    /**
     * Form name to use
     *
     * @var string
     */
    protected $name;

    /**
     * @param Controller       $controller
     * @param string           $name
     */
    public function __construct($controller = null, $name = 'ReviewContentForm')
    {
        $this->controller = $controller;
        $this->name = $name;
    }

    /**
     * Bootstrap the form fields for the content review modal
     *
     * @param DataObject $object
     * @return Form
     */
    public function Form($object)
    {
        $placeholder = _t(__CLASS__ . '.Placeholder', 'Add comments (optional)');
        $title = _t(__CLASS__ . '.MarkAsReviewedAction', 'Mark as reviewed');

        $fields = FieldList::create([
            HiddenField::create('ID', null, $object->ID),
            HiddenField::create('ClassName', null, $object->baseClass()),
            TextareaField::create('Review', '')
                ->setAttribute('placeholder', $placeholder)
                ->setSchemaData(['attributes' => ['placeholder' => $placeholder]])
        ]);

        $action = FormAction::create('savereview', $title)
            ->setTitle($title)
            ->setUseButtonTag(false)
            ->addExtraClass('review-content__action btn btn-primary');
        $actions = FieldList::create([$action]);

        $form = Form::create($this->controller, $this->name, $fields, $actions)
            ->setHTMLID('Form_EditForm_ReviewContent')
            ->addExtraClass('form--no-dividers review-content__form');

        return $form;
    }

    /**
     * Validate, and save the submitted form's review
     *
     * @param  DataObject $record
     * @param  array $data
     * @return HTTPResponse|string
     * @throws ValidationException If the user cannot submit the review
     */
    public function submitReview($record, $data)
    {
        /** @var DataObject|SiteTreeContentReview $record */
        if (!$this->canSubmitReview($record)) {
            throw new ValidationException(_t(
                __CLASS__ . '.ErrorReviewPermissionDenied',
                'It seems you don\'t have the necessary permissions to submit a content review'
            ));
        }

        $notes = (!empty($data['Review']) ? $data['Review'] : _t(__CLASS__ . '.NoComments', '(no comments)'));
        $record->addReviewNote(Security::getCurrentUser(), $notes);
        $record->advanceReviewDate();

        $request = $this->controller->getRequest();
        $message = _t(__CLASS__ . '.Success', 'Review successfully added');

        if ($request->getHeader('X-Formschema-Request')) {
            return $message;
        } elseif (Director::is_ajax()) {
            $response = HTTPResponse::create($message, 200);
            $response->addHeader('Content-Type', 'text/html; charset=utf-8');
            return $response;
        }

        return $this->controller->redirectBack();
    }

    /**
     * Determine whether the user can submit a review
     *
     * @param DataObject $record
     * @return bool
     */
    public function canSubmitReview($record)
    {
        if (!$record->canEdit()
            || !$record->hasMethod('canBeReviewedBy')
            || !$record->canBeReviewedBy(Security::getCurrentUser())
        ) {
            return false;
        }
        return true;
    }
}


<?php

namespace SilverStripe\ContentReview\Extensions;

use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Admin\LeftAndMainExtension;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\ContentReview\Forms\ReviewContentHandler;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Security;

/**
 * CMSPageEditController extension to receive the additional action button from
 * SiteTreeContentReview::updateCMSActions()
 */
class ContentReviewCMSExtension extends LeftAndMainExtension
{
    private static $allowed_actions = [
        'ReviewContentForm',
        'savereview',
    ];

    /**
     * URL handler for the "content due for review" form
     *
     * @param HTTPRequest $request
     * @return Form|null
     */
    public function ReviewContentForm(HTTPRequest $request)
    {
        // Get ID either from posted back value, or url parameter
        $id = $request->param('ID') ?: $request->postVar('ID');
        return $this->getReviewContentForm($id);
    }

    /**
     * Return a handler for "content due for review" forms, according to the given object ID
     *
     * @param  int $id
     * @return Form|null
     */
    public function getReviewContentForm($id)
    {
        $page = $this->findRecord(['ID' => $id]);
        $user = Security::getCurrentUser();
        if (!$page->canEdit() || ($page->hasMethod('canBeReviewedBy') && !$page->canBeReviewedBy($user))) {
            $this->owner->httpError(403, _t(
                __CLASS__.'.ErrorItemPermissionDenied',
                'It seems you don\'t have the necessary permissions to review this content'
            ));
            return null;
        }

        $form = $this->getReviewContentHandler()->Form($page);
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) {
            $schemaId = $this->owner->join_links($this->owner->Link('schema/ReviewContentForm'), $id);
            return $this->getSchemaResponse($schemaId, $form, $errors);
        });

        return $form;
    }

    /**
     * Action handler for processing the submitted content review
     *
     * @param array $data
     * @param Form $form
     * @return DBHTMLText|HTTPResponse|null
     */
    public function savereview($data, Form $form)
    {
        $page = $this->findRecord($data);

        $results = $this->getReviewContentHandler()->submitReview($page, $data);
        if (is_null($results)) {
            return null;
        }
        if ($this->getSchemaRequested()) {
            // Send extra "message" data with schema response
            $extraData = ['message' => $results];
            $schemaId = $this->owner->join_links($this->owner->Link('schema/ReviewContentForm'), $page->ID);
            return $this->getSchemaResponse($schemaId, $form, null, $extraData);
        }

        return $results;
    }

    /**
     * Return a handler or reviewing content
     *
     * @return ReviewContentHandler
     */
    protected function getReviewContentHandler()
    {
        return ReviewContentHandler::create($this->owner);
    }

    /**
     * Find the page this form is updating
     *
     * @param array $data Form data
     * @return SiteTree Record
     * @throws HTTPResponse_Exception
     */
    protected function findRecord($data)
    {
        if (empty($data["ID"])) {
            throw new HTTPResponse_Exception("No record ID", 404);
        }
        $page = null;
        $id = $data["ID"];
        if (is_numeric($id)) {
            $page = SiteTree::get()->byID($id);
        }
        if (!$page || !$page->ID) {
            throw new HTTPResponse_Exception("Bad record ID #{$id}", 404);
        }
        return $page;
    }

    /**
     * Check if the current request has a X-Formschema-Request header set.
     * Used by conditional logic that responds to validation results
     *
     * @todo Remove duplication. See https://github.com/silverstripe/silverstripe-admin/issues/240
     *
     * @return bool
     */
    protected function getSchemaRequested()
    {
        $parts = $this->owner->getRequest()->getHeader(LeftAndMain::SCHEMA_HEADER);
        return !empty($parts);
    }

    /**
     * Generate schema for the given form based on the X-Formschema-Request header value
     *
     * @todo Remove duplication. See https://github.com/silverstripe/silverstripe-admin/issues/240
     *
     * @param string $schemaID ID for this schema. Required.
     * @param Form $form Required for 'state' or 'schema' response
     * @param ValidationResult $errors Required for 'error' response
     * @param array $extraData Any extra data to be merged with the schema response
     * @return HTTPResponse
     */
    protected function getSchemaResponse($schemaID, $form = null, ValidationResult $errors = null, $extraData = [])
    {
        $parts = $this->owner->getRequest()->getHeader(LeftAndMain::SCHEMA_HEADER);
        $data = $this->owner
            ->getFormSchema()
            ->getMultipartSchema($parts, $schemaID, $form, $errors);

        if ($extraData) {
            $data = array_merge($data, $extraData);
        }

        $response = HTTPResponse::create(Convert::raw2json($data));
        $response->addHeader('Content-Type', 'application/json');

        return $response;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Abhimanyu
 * Date: 11-02-2015
 * Time: 23:11
 */

namespace abhimanyu\user\models;

use yii\base\NotSupportedException;
use yii\web\IdentityInterface;

class UserIdentity extends User implements IdentityInterface
{
	/**
	 * Finds an identity by the given ID.
	 *
	 * @param string|integer $id the ID to be looked for
	 *
	 * @return IdentityInterface the identity object that matches the given ID.
	 * Null should be returned if such an identity cannot be found
	 * or the identity is not in an active state (disabled, deleted, etc.)
	 */
	public static function findIdentity($id)
	{
		return static::findOne(['id' => $id, 'status' => User::STATUS_ACTIVE]);
	}

	/**
	 * Finds an identity by the given token.
	 *
	 * @param mixed $token the token to be looked for
	 * @param mixed $type  the type of the token. The value of this parameter depends on the implementation.
	 *                     For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be
	 *                     `yii\filters\auth\HttpBearerAuth`.
	 *
	 * @return \yii\web\IdentityInterface the identity object that matches the given token.
	 * Null should be returned if such an identity cannot be found
	 * or the identity is not in an active state (disabled, deleted, etc.)
	 * @throws \yii\base\NotSupportedException
	 */
	public static function findIdentityByAccessToken($token, $type = NULL)
	{
		throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
	}

	/**
	 * Finds user by username
	 *
	 * @param string $username
	 *
	 * @return static|null
	 */
	public static function findByUsername($username)
	{
		return static::findOne(['username' => $username, 'status' => self::STATUS_ACTIVE]);
	}

	/**
	 * Finds user by either username or email
	 *
	 * @param string $emailOrUsername
	 *
	 * @return \abhimanyu\user\models\UserIdentity|null
	 */
	public static function findByUsernameOrEmail($emailOrUsername)
	{
		if (filter_var($emailOrUsername, FILTER_VALIDATE_EMAIL)) {
			return UserIdentity::findByEmail($emailOrUsername);
		}

		return UserIdentity::findByUsername($emailOrUsername);
	}

	/**
	 * Finds user by email
	 *
	 * @param string $email
	 *
	 * @return null|static
	 */
	public static function findByEmail($email)
	{
		return static::findOne(['email' => $email, 'status' => self::STATUS_ACTIVE]);
	}

	/**
	 * Finds user by password reset token
	 *
	 * @param integer $id   user id
	 * @param string  $code password reset token
	 *
	 * @return null|static
	 */
	public static function findByPasswordResetToken($id, $code)
	{
		if (!static::isPasswordResetTokenValid($code)) {
			return NULL;
		}

		return static::findOne([
			'id'                   => $id,
			'password_reset_token' => $code,
			'status'               => self::STATUS_ACTIVE,
		]);
	}

	/**
	 * Find User by activation token
	 *
	 * @param integer $id   User Id
	 * @param string  $code User Activation Token
	 *
	 * @return static
	 */
	public static function findByActivationToken($id, $code)
	{
		return static::findOne([
			'id'               => $id,
			'activation_token' => $code,
			'status'           => self::STATUS_PENDING
		]);
	}

	/**
	 * Returns an ID that can uniquely identify a user identity.
	 *
	 * @return string|integer an ID that uniquely identifies a user identity.
	 */
	public function getId()
	{
		return $this->getPrimaryKey();
	}

	/**
	 * Validates the given auth key.
	 *
	 * This is required if [[User::enableAutoLogin]] is enabled.
	 *
	 * @param string $authKey the given auth key
	 *
	 * @return boolean whether the given auth key is valid.
	 * @see getAuthKey()
	 */
	public function validateAuthKey($authKey)
	{
		return $this->getAuthKey() === $authKey;
	}

	/**
	 * Returns a key that can be used to check the validity of a given identity ID.
	 *
	 * The key should be unique for each individual user, and should be persistent
	 * so that it can be used to check the validity of the user identity.
	 *
	 * The space of such keys should be big enough to defeat potential identity attacks.
	 *
	 * This is required if [[User::enableAutoLogin]] is enabled.
	 *
	 * @return string a key that is used to check the validity of a given identity ID.
	 * @see validateAuthKey()
	 */
	public function getAuthKey()
	{
		return $this->auth_key;
	}
}


<?php
	/**
	 * Created by PhpStorm.
	 * User: Abhimanyu
	 * Date: 13-02-2015
	 * Time: 18:19
	 */

	namespace abhimanyu\user\controllers;

	use abhimanyu\user\models\Profile;
	use Yii;
	use yii\filters\AccessControl;
	use yii\web\Controller;
	use yii\web\NotFoundHttpException;

	/**
	 * ProfileController shows users profiles.
	 */
	class ProfileController extends Controller
	{
		public function behaviors()
		{
			return [
				'access' => [
					'class' => AccessControl::className(),
					'rules' => [
						[
							'allow'   => TRUE,
							'actions' => ['index'],
							'roles'   => ['@']
						]
					]
				],
			];
		}

		/**
		 * Shows user's profile.
		 *
		 * @return \yii\web\Response
		 * @throws \yii\web\NotFoundHttpException
		 */
		public function actionIndex()
		{
			$profile = Profile::findOne(['uid' => Yii::$app->user->id]);

			if ($profile == NULL)
				throw new NotFoundHttpException;

			return $this->render('index', ['profile' => $profile]);
		}
	}


<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 * (c) Armin Ronacher
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Exception thrown when a syntax error occurs during lexing or parsing of a template.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Twig_Error_Syntax extends Twig_Error
{
    /**
     * Tweaks the error message to include suggestions.
     *
     * @param string $name  The original name of the item that does not exist
     * @param array  $items An array of possible items
     */
    public function addSuggestions($name, array $items)
    {
        if (!$alternatives = self::computeAlternatives($name, $items)) {
            return;
        }

        $this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', $alternatives)));
    }

    /**
     * @internal
     *
     * To be merged with the addSuggestions() method in 2.0.
     */
    public static function computeAlternatives($name, $items)
    {
        $alternatives = array();
        foreach ($items as $item) {
            $lev = levenshtein($name, $item);
            if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
                $alternatives[$item] = $lev;
            }
        }
        asort($alternatives);

        return array_keys($alternatives);
    }
}

class_alias('Twig_Error_Syntax', 'Twig\Error\SyntaxError', false);


<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Twig_Node_SandboxedPrint adds a check for the __toString() method
 * when the variable is an object and the sandbox is activated.
 *
 * When there is a simple Print statement, like {{ article }},
 * and if the sandbox is enabled, we need to check that the __toString()
 * method is allowed if 'article' is an object.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Twig_Node_SandboxedPrint extends Twig_Node_Print
{
    public function compile(Twig_Compiler $compiler)
    {
        $compiler
            ->addDebugInfo($this)
            ->write('echo $this->env->getExtension(\'Twig_Extension_Sandbox\')->ensureToStringAllowed(')
            ->subcompile($this->getNode('expr'))
            ->raw(");\n")
        ;
    }

    /**
     * Removes node filters.
     *
     * This is mostly needed when another visitor adds filters (like the escaper one).
     *
     * @return Twig_Node
     */
    protected function removeNodeFilter(Twig_Node $node)
    {
        if ($node instanceof Twig_Node_Expression_Filter) {
            return $this->removeNodeFilter($node->getNode('node'));
        }

        return $node;
    }
}

class_alias('Twig_Node_SandboxedPrint', 'Twig\Node\SandboxedPrintNode', false);


<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Twig base exception.
 *
 * This exception class and its children must only be used when
 * an error occurs during the loading of a template, when a syntax error
 * is detected in a template, or when rendering a template. Other
 * errors must use regular PHP exception classes (like when the template
 * cache directory is not writable for instance).
 *
 * To help debugging template issues, this class tracks the original template
 * name and line where the error occurred.
 *
 * Whenever possible, you must set these information (original template name
 * and line number) yourself by passing them to the constructor. If some or all
 * these information are not available from where you throw the exception, then
 * this class will guess them automatically (when the line number is set to -1
 * and/or the name is set to null). As this is a costly operation, this
 * can be disabled by passing false for both the name and the line number
 * when creating a new instance of this class.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Twig_Error extends Exception
{
    protected $lineno;
    // to be renamed to name in 2.0
    protected $filename;
    protected $rawMessage;
    protected $previous;

    private $sourcePath;
    private $sourceCode;

    /**
     * Constructor.
     *
     * Set both the line number and the name to false to
     * disable automatic guessing of the original template name
     * and line number.
     *
     * Set the line number to -1 to enable its automatic guessing.
     * Set the name to null to enable its automatic guessing.
     *
     * By default, automatic guessing is enabled.
     *
     * @param string                  $message  The error message
     * @param int                     $lineno   The template line where the error occurred
     * @param Twig_Source|string|null $source   The source context where the error occurred
     * @param Exception               $previous The previous exception
     */
    public function __construct($message, $lineno = -1, $source = null, Exception $previous = null)
    {
        if (null === $source) {
            $name = null;
        } elseif (!$source instanceof Twig_Source) {
            // for compat with the Twig C ext., passing the template name as string is accepted
            $name = $source;
        } else {
            $name = $source->getName();
            $this->sourceCode = $source->getCode();
            $this->sourcePath = $source->getPath();
        }
        if (PHP_VERSION_ID < 50300) {
            $this->previous = $previous;
            parent::__construct('');
        } else {
            parent::__construct('', 0, $previous);
        }

        $this->lineno = $lineno;
        $this->filename = $name;

        if (-1 === $lineno || null === $name || null === $this->sourcePath) {
            $this->guessTemplateInfo();
        }

        $this->rawMessage = $message;

        $this->updateRepr();
    }

    /**
     * Gets the raw message.
     *
     * @return string The raw message
     */
    public function getRawMessage()
    {
        return $this->rawMessage;
    }

    /**
     * Gets the logical name where the error occurred.
     *
     * @return string The name
     *
     * @deprecated since 1.27 (to be removed in 2.0). Use getSourceContext() instead.
     */
    public function getTemplateFile()
    {
        @trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use getSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);

        return $this->filename;
    }

    /**
     * Sets the logical name where the error occurred.
     *
     * @param string $name The name
     *
     * @deprecated since 1.27 (to be removed in 2.0). Use setSourceContext() instead.
     */
    public function setTemplateFile($name)
    {
        @trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use setSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);

        $this->filename = $name;

        $this->updateRepr();
    }

    /**
     * Gets the logical name where the error occurred.
     *
     * @return string The name
     *
     * @deprecated since 1.29 (to be removed in 2.0). Use getSourceContext() instead.
     */
    public function getTemplateName()
    {
        @trigger_error(sprintf('The "%s" method is deprecated since version 1.29 and will be removed in 2.0. Use getSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);

        return $this->filename;
    }

    /**
     * Sets the logical name where the error occurred.
     *
     * @param string $name The name
     *
     * @deprecated since 1.29 (to be removed in 2.0). Use setSourceContext() instead.
     */
    public function setTemplateName($name)
    {
        @trigger_error(sprintf('The "%s" method is deprecated since version 1.29 and will be removed in 2.0. Use setSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);

        $this->filename = $name;
        $this->sourceCode = $this->sourcePath = null;

        $this->updateRepr();
    }

    /**
     * Gets the template line where the error occurred.
     *
     * @return int The template line
     */
    public function getTemplateLine()
    {
        return $this->lineno;
    }

    /**
     * Sets the template line where the error occurred.
     *
     * @param int $lineno The template line
     */
    public function setTemplateLine($lineno)
    {
        $this->lineno = $lineno;

        $this->updateRepr();
    }

    /**
     * Gets the source context of the Twig template where the error occurred.
     *
     * @return Twig_Source|null
     */
    public function getSourceContext()
    {
        return $this->filename ? new Twig_Source($this->sourceCode, $this->filename, $this->sourcePath) : null;
    }

    /**
     * Sets the source context of the Twig template where the error occurred.
     */
    public function setSourceContext(Twig_Source $source = null)
    {
        if (null === $source) {
            $this->sourceCode = $this->filename = $this->sourcePath = null;
        } else {
            $this->sourceCode = $source->getCode();
            $this->filename = $source->getName();
            $this->sourcePath = $source->getPath();
        }

        $this->updateRepr();
    }

    public function guess()
    {
        $this->guessTemplateInfo();
        $this->updateRepr();
    }

    /**
     * For PHP < 5.3.0, provides access to the getPrevious() method.
     *
     * @param string $method    The method name
     * @param array  $arguments The parameters to be passed to the method
     *
     * @return Exception The previous exception or null
     *
     * @throws BadMethodCallException
     */
    public function __call($method, $arguments)
    {
        if ('getprevious' == strtolower($method)) {
            return $this->previous;
        }

        throw new BadMethodCallException(sprintf('Method "Twig_Error::%s()" does not exist.', $method));
    }

    public function appendMessage($rawMessage)
    {
        $this->rawMessage .= $rawMessage;
        $this->updateRepr();
    }

    /**
     * @internal
     */
    protected function updateRepr()
    {
        $this->message = $this->rawMessage;

        if ($this->sourcePath && $this->lineno > 0) {
            $this->file = $this->sourcePath;
            $this->line = $this->lineno;

            return;
        }

        $dot = false;
        if ('.' === substr($this->message, -1)) {
            $this->message = substr($this->message, 0, -1);
            $dot = true;
        }

        $questionMark = false;
        if ('?' === substr($this->message, -1)) {
            $this->message = substr($this->message, 0, -1);
            $questionMark = true;
        }

        if ($this->filename) {
            if (is_string($this->filename) || (is_object($this->filename) && method_exists($this->filename, '__toString'))) {
                $name = sprintf('"%s"', $this->filename);
            } else {
                $name = json_encode($this->filename);
            }
            $this->message .= sprintf(' in %s', $name);
        }

        if ($this->lineno && $this->lineno >= 0) {
            $this->message .= sprintf(' at line %d', $this->lineno);
        }

        if ($dot) {
            $this->message .= '.';
        }

        if ($questionMark) {
            $this->message .= '?';
        }
    }

    /**
     * @internal
     */
    protected function guessTemplateInfo()
    {
        $template = null;
        $templateClass = null;

        if (PHP_VERSION_ID >= 50306) {
            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
        } else {
            $backtrace = debug_backtrace();
        }

        foreach ($backtrace as $trace) {
            if (isset($trace['object']) && $trace['object'] instanceof Twig_Template && 'Twig_Template' !== get_class($trace['object'])) {
                $currentClass = get_class($trace['object']);
                $isEmbedContainer = 0 === strpos($templateClass, $currentClass);
                if (null === $this->filename || ($this->filename == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
                    $template = $trace['object'];
                    $templateClass = get_class($trace['object']);
                }
            }
        }

        // update template name
        if (null !== $template && null === $this->filename) {
            $this->filename = $template->getTemplateName();
        }

        // update template path if any
        if (null !== $template && null === $this->sourcePath) {
            $src = $template->getSourceContext();
            $this->sourceCode = $src->getCode();
            $this->sourcePath = $src->getPath();
        }

        if (null === $template || $this->lineno > -1) {
            return;
        }

        $r = new ReflectionObject($template);
        $file = $r->getFileName();

        $exceptions = array($e = $this);
        while (($e instanceof self || method_exists($e, 'getPrevious')) && $e = $e->getPrevious()) {
            $exceptions[] = $e;
        }

        while ($e = array_pop($exceptions)) {
            $traces = $e->getTrace();
            array_unshift($traces, array('file' => $e->getFile(), 'line' => $e->getLine()));

            while ($trace = array_shift($traces)) {
                if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
                    continue;
                }

                foreach ($template->getDebugInfo() as $codeLine => $templateLine) {
                    if ($codeLine <= $trace['line']) {
                        // update template line
                        $this->lineno = $templateLine;

                        return;
                    }
                }
            }
        }
    }
}

class_alias('Twig_Error', 'Twig\Error\Error', false);
class_exists('Twig_Source');


<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Exposes a template to userland.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Twig_TemplateWrapper
{
    private $env;
    private $template;

    /**
     * This method is for internal use only and should never be called
     * directly (use Twig_Environment::load() instead).
     *
     * @internal
     */
    public function __construct(Twig_Environment $env, Twig_Template $template)
    {
        $this->env = $env;
        $this->template = $template;
    }

    /**
     * Renders the template.
     *
     * @param array $context An array of parameters to pass to the template
     *
     * @return string The rendered template
     */
    public function render($context = array())
    {
        return $this->template->render($context);
    }

    /**
     * Displays the template.
     *
     * @param array $context An array of parameters to pass to the template
     */
    public function display($context = array())
    {
        $this->template->display($context);
    }

    /**
     * Checks if a block is defined.
     *
     * @param string $name    The block name
     * @param array  $context An array of parameters to pass to the template
     *
     * @return bool
     */
    public function hasBlock($name, $context = array())
    {
        return $this->template->hasBlock($name, $context);
    }

    /**
     * Returns defined block names in the template.
     *
     * @param array $context An array of parameters to pass to the template
     *
     * @return string[] An array of defined template block names
     */
    public function getBlockNames($context = array())
    {
        return $this->template->getBlockNames($context);
    }

    /**
     * Renders a template block.
     *
     * @param string $name    The block name to render
     * @param array  $context An array of parameters to pass to the template
     *
     * @return string The rendered block
     */
    public function renderBlock($name, $context = array())
    {
        $context = $this->env->mergeGlobals($context);
        $level = ob_get_level();
        ob_start();
        try {
            $this->template->displayBlock($name, $context);
        } catch (Exception $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }

            throw $e;
        } catch (Throwable $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }

            throw $e;
        }

        return ob_get_clean();
    }

    /**
     * Displays a template block.
     *
     * @param string $name    The block name to render
     * @param array  $context An array of parameters to pass to the template
     */
    public function displayBlock($name, $context = array())
    {
        $this->template->displayBlock($name, $this->env->mergeGlobals($context));
    }

    /**
     * @return Twig_Source
     */
    public function getSourceContext()
    {
        return $this->template->getSourceContext();
    }
}

class_alias('Twig_TemplateWrapper', 'Twig\TemplateWrapper', false);