Module.php #1

  • //
  • guest/
  • thomas_gray/
  • jambox/
  • main/
  • swarm/
  • module/
  • Workshop/
  • Module.php
  • View
  • Commits
  • Open Download .zip Download (21 KB)
<?php
/**
 * Perforce Swarm
 *
 * @copyright   2014 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */

namespace Workshop;

use Application\Filter\StringToId;
use P4\Filter\Utf8;
use Projects\Validator\BranchPath as BranchPathValidator;
use Projects\Model\Project as Project;
use Users\Model\User;
use Zend\Console\Request as ConsoleRequest;
use Zend\Mvc\MvcEvent;
use Zend\View\Model\JsonModel;

class Module
{
    /**
     * Bootstrap events.
     *
     * @param   MvcEvent    $event  the bootstrap event
     * @return  void
     */
    public function onBootstrap(MvcEvent $event)
    {
        $application = $event->getApplication();

        // attach to project add/delete events
        $events = $application->getEventManager();
        $events->attach(
            array(MvcEvent::EVENT_FINISH),
            array($this, 'updateJobSpec'),
            -200
        );

        // update the project filter
        $this->updateProjectFilter($event);

        // update the job spec to Workshop standards
        $events->attach(
            array(MvcEvent::EVENT_ROUTE),
            array($this, 'setJobSpec'),
            -200
        );
    }

    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }

    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }

    /**
     * If the job spec does not match the expected workshop spec, update it.
     *
     * @param MvcEvent $event
     */
    public function setJobSpec(MvcEvent $event)
    {
        $routeMatch = $event->getRouteMatch();

        if (!$routeMatch) {
            return;
        }

        $application = $event->getApplication();
        $services    = $application->getServiceManager();
        $p4          = $services->get('p4');

        if (!$p4) {
            return;
        }

        $route    = $routeMatch->getMatchedRouteName();
        $request  = $event->getRequest();
        if ($request instanceof ConsoleRequest) {
            return;
        }

        $method = $request->getMethod();

        // only proceed if submitting the add project form or forking a project
        if (!(($route == 'projectAddAvatar' || $route == 'add-project') && $method == 'POST')
            && !($route == 'forkProject' && $method == 'GET')) {
            return;
        }

        $spec   = \P4\Spec\Definition::fetch('job', $p4);
        $fields = $spec->getFields();

        if (array_key_exists('Project', $fields)) {
            return;
        }

        // remove these unused fields
        unset($fields['User']);
        unset($fields['Date']);

        $fields['Project'] = array (
            'code' => '106',
            'dataType' => 'select',
            'options'   => '',
            'displayLength' => '10',
            'fieldType' => 'required',
            'default' => 'setme',
        );
        $fields['Severity'] = array (
            'code' => '109',
            'dataType' => 'select',
            'options'   => array('A', 'B', 'C'),
            'displayLength' => '10',
            'fieldType' => 'required',
            'default' => 'C',
        );
        $fields['ReportedBy'] = array (
            'code' => '103',
            'dataType' => 'word',
            'displayLength' => '32',
            'fieldType' => 'required',
            'default' => '$user',
        );
        $fields['ReportedDate'] = array (
            'code' => '104',
            'dataType' => 'date',
            'displayLength' => '20',
            'fieldType' => 'once',
            'default' => '$now',
        );
        $fields['ModifiedBy'] = array (
            'code' => '110',
            'dataType' => 'word',
            'displayLength' => '20',
            'fieldType' => 'always',
            'default' => '$user',
        );
        $fields['ModifiedDate'] = array (
            'code' => '111',
            'dataType' => 'date',
            'displayLength' => '20',
            'fieldType' => 'always',
            'default' => '$now',
        );
        $fields['OwnedBy'] = array (
            'code' => '108',
            'dataType' => 'word',
            'displayLength' => '32',
            'fieldType' => 'required',
        );
        $fields['DevNotes'] = array (
            'code' => '107',
            'dataType' => 'text',
            'displayLength' => '0',
            'fieldType' => 'optional',
        );
        $fields['Type']  = array (
            'code' => '112',
            'dataType' => 'select',
            'options'   => array('Bug', 'Feature'),
            'displayLength' => '7',
            'fieldType' => 'required',
            'default' => 'Bug',
        );

        $spec->setFields($fields);
        $spec->save();

        // set the list of job ids
        $this->updateJobSpec($event);
    }

    /**
     * Modify the project filter to:
     *  - allow and set the default value for the creator field on add
     *  - enforce the Workshop naming scheme for projects
     *  - enforce the Workshop id scheme for projects
     *  - fill in the jobspec for projects, if empty
     *
     * @param MvcEvent $event
     */
    public function updateProjectFilter(MvcEvent $event)
    {
        $application = $event->getApplication();
        $services    = $application->getServiceManager();
        $toId        = new StringToId;

        // Modify the project filter
        $filters        = $services->get('InputFilterManager');
        $p4             = $services->get('p4');
        $projectFilter  = $filters->get('ProjectFilter');

        // callback to create branch id from name
        $toBranchId = function ($name) {
            // don't just use $toId->filter() as we want to allow . and _ characters
            // Attempt to replace uppercase unicode with dashes
            // if the mbstring extension is not installed.
            $utf8  = new Utf8;
            $id = function_exists('mb_strtolower')
                ? mb_strtolower($name, 'UTF-8')
                : strtolower($name);
            $id = preg_replace(
                '/[�\p{Lu}]+/u',
                '-',
                $utf8->filter($id)
            );
            // replace anything except the matching characters with - characters
            $id = trim(
                preg_replace('/[^a-z0-9\x80-\xFF\_\.]+/', '-', $id),
                '-'
            );
            return $id;
        };

        // copy callback to validate users (as its used on multiple elements)
        $usersValidatorCallback = function ($value) use ($p4) {
            if (in_array(false, array_map('is_string', $value))) {
                return 'User ids must be strings';
            }

            $unknownIds = array_diff($value, User::exists($value, $p4));
            if (count($unknownIds)) {
                return 'Unknown user id(s) ' . implode(', ', $unknownIds);
            }

            return true;
        };

        $generateProjectId = function ($value = null) use ($p4, $projectFilter, $toId, $services) {
            if ($value === null) {
                $value = $toId($projectFilter->getRawValue('name'));
            }
            if ($projectFilter->getMode() !== $projectFilter::MODE_ADD) {
                $user = Project::fetch($projectFilter->getRawValue('id'), $p4)->get('creator');
            } else {
                $user = $services->get('user')->getId();
            }
            return $user . '-' . $toId($value);
        };

        // add the creator field and set its value to the current user's id, if we're creating a project
        // if editing the project, use the provided id to fetch the project's creator and return it, ensuring
        // the creator can never be changed by the user
        $projectFilter->add(
            array(
                'name'          => 'creator',
                'required'      => true,
                'filters'       => array(
                    array(
                        'name'      => 'Callback',
                        'options'   => array(
                            'callback' => function ($value) use ($projectFilter, $services) {
                                if ($projectFilter->getMode() !== $projectFilter::MODE_ADD) {
                                    $p4Admin        = $services->get('p4admin');
                                    $id             = $projectFilter->getRawValue('id');
                                    $project        = Project::fetch($id, $p4Admin);
                                    $currentCreator = $project->get('creator');
                                    return $currentCreator;
                                }
                                $user = $services->get('user');
                                return $user->getId();
                            }
                        )
                    )
                ),
            )
        );

        // prepend the id with the current user's id if adding a project
        // only run through StringToId if adding, already verified on edit.
        $projectFilter->remove('id')->add(
            array(
                'name'      => 'id',
                'filters'   => array(
                    array(
                        'name'      => 'Callback',
                        'options'   => array(
                            'callback' => function ($value) use ($projectFilter, $generateProjectId) {
                                if ($projectFilter->getMode() !== $projectFilter::MODE_ADD) {
                                    return $value;
                                }
                                return $generateProjectId($value);
                            }
                        )
                    )
                )
            )
        );

        // copied from original job filter
        $reserved = array('add', 'edit', 'delete');

        // ensure name is given and produces a usable/unique id.
        $projectFilter->remove('name')->add(
            array(
                'name'          => 'name',
                'filters'       => array('trim'),
                'validators'    => array(
                    array(
                        'name'      => 'NotEmpty',
                        'options'   => array(
                            'message'   =>  "Name is required and can't be empty."
                        )
                    ),
                    array(
                        'name'      => '\Application\Validator\Callback',
                        'options'   => array(
                            'callback'  => function ($value) use ($p4, $reserved, $projectFilter, $generateProjectId) {
                                if (empty($value)) {
                                    return 'Name must contain at least one letter or number.';
                                }

                                // if it isn't an add, we assume the caller will take care
                                // of ensuring existence.
                                if ($projectFilter->getMode() !== $projectFilter::MODE_ADD) {
                                    return true;
                                }

                                $id = $generateProjectId($value);

                                // try to get project (including deleted) matching the name
                                $matchingProjects = Project::fetchAll(
                                    array(
                                        Project::FETCH_INCLUDE_DELETED => true,
                                        Project::FETCH_BY_IDS          => array($id)
                                    ),
                                    $p4
                                );

                                if ($matchingProjects->count() || in_array($id, $reserved)) {
                                    return 'This name is taken. Please pick a different name.';
                                }

                                return true;
                            }
                        )
                    )
                )
            )
        );

        $projectFilter->remove('branches')->add(
            array(
                'name'          => 'branches',
                'required'      => false,
                'filters'   => array(
                    array(
                        'name'  => 'Callback',
                        'options'   => array(
                            'callback'  => function ($value) use ($p4, $toId, $toBranchId, $projectFilter, $services) {
                                // normalize the posted branch details to only contain our expected keys
                                // also, generate an id (based on name) for entries lacking one
                                $normalized = array();
                                $defaults   = array(
                                    'id'            => null,
                                    'name'          => null,
                                    'paths'         => '',
                                    'moderators'    => array()
                                );

                                // do not use generateProjectId as we don't want the user prepended
                                $project = $toId($projectFilter->getRawValue('name'));
                                if ($projectFilter->getMode() == $projectFilter::MODE_ADD) {
                                    $user = $services->get('user')->getId();
                                } else {
                                    $user = Project::fetch($projectFilter->getRawValue('id'), $p4)->get('creator');
                                }

                                foreach ((array) $value as $branch) {
                                    $branch = (array) $branch + $defaults;
                                    $branch = array_intersect_key($branch, $defaults);

                                    if (!strlen($branch['id'])
                                        || $projectFilter->getMode() == $projectFilter::MODE_ADD) {
                                        $branch['id'] = $toBranchId($branch['name']);
                                    }

                                    // turn our paths text input into an array based on Workshop rules
                                    $branch['paths'] = array(
                                        '//guest/' . $user . '/' . $project . '/' . $branch['id'] . '/...'
                                    );

                                    $normalized[] = $branch;
                                }

                                return $normalized;
                            }
                        )
                    )
                ),
                'validators'    => array(
                    array(
                        'name'      => '\Application\Validator\Callback',
                        'options'   => array(
                            'callback'  => function ($value) use ($usersValidatorCallback, $p4) {
                                // ensure all branches have a name and id.
                                // also ensure that no id is used more than once.
                                $ids        = array();
                                $branchPath = new BranchPathValidator(array('connection' => $p4));
                                foreach ((array) $value as $branch) {
                                    if (!strlen($branch['name'])) {
                                        return "All branches require a name.";
                                    }

                                    // given our normalization, we assume an empty id results from a bad name
                                    if (!strlen($branch['id'])) {
                                        return 'Branch name must contain at least one letter or number.';
                                    }

                                    if (in_array($branch['id'], $ids)) {
                                        return "Two branches cannot have the same id. '"
                                        . $branch['id'] . "' is already in use for this project.";
                                    }

                                    // validate branch paths
                                    if (!$branchPath->isValid($branch['paths'])) {
                                        return "Error in '" . $branch['name'] . "' branch: "
                                        . implode(' ', $branchPath->getMessages());
                                    }

                                    // verify branch moderators
                                    $moderatorsCheck = $usersValidatorCallback($branch['moderators']);
                                    if ($moderatorsCheck !== true) {
                                        return $moderatorsCheck;
                                    }

                                    $ids[] = $branch['id'];
                                }

                                return true;
                            }
                        )
                    )
                )
            )
        );

        // enforce Workshop naming scheme
        $projectFilter->add(
            array(
                'name'          => 'name',
                'validators'    => array(
                    array(
                        'name'=> 'regex',
                        'options'   => array(
                            'pattern'   => '/^[\w\s\-]+$/',
                            'message'   => 'Name must contain only alphanumeric and underscore characters.',
                        )
                    )
                )
            )
        );

        // replace default jobview validator to fill in if empty
        $projectFilter->remove('jobview')->add(
            array(
                'name'         => 'jobview',
                'required'     => false,
                'filters'      => array(
                    'trim',
                    array(
                        'name'  => 'Callback',
                        'options'   => array(
                            'callback'  => function ($value) use ($generateProjectId) {
                                if (empty($value)) {
                                    return 'project=' . $generateProjectId();
                                }
                                return $value;
                            }
                        )
                    )
                ),
                'validators'   => array(
                    array(
                        'name'      => '\Application\Validator\Callback',
                        'options'   => array(
                            'callback'  => function ($value) {
                                if (!strlen($value)) {
                                    return true;
                                }

                                $filters = preg_split('/\s+/', $value);
                                foreach ($filters as $filter) {
                                    if (!preg_match('/^([^=()|]+)=([^=()|]+)$/', $filter)) {
                                        return "Job filter only supports key=value conditions and the '*' wildcard.";
                                    }
                                }

                                return true;
                            }
                        )
                    )
                )
            )
        );

        $filters->setService('ProjectFilter', $projectFilter);
    }

    /**
     * For the add-project route, only when the form is submitted (vs displayed)
     * OR for the delete-project route,
     * OR for the fork route,
     * if the response is JSON formatted and the result is valid,
     * then update the job spec with the list of current projects.
     *
     * @param MvcEvent $event
     */
    public function updateJobSpec(MvcEvent $event)
    {
        $routeMatch = $event->getRouteMatch();

        if (!$routeMatch) {
            return;
        }

        $application = $event->getApplication();
        $services    = $application->getServiceManager();
        $p4Admin     = $services->get('p4_admin');

        if (!$p4Admin) {
            return;
        }

        $route    = $routeMatch->getMatchedRouteName();
        $request  = $event->getRequest();

        if ($request instanceof ConsoleRequest) {
            return;
        }
        
        $method   = $request->getMethod();
        $result   = $event->getResult();

        if ((($route == 'add-project' && $method == 'POST') || $route == 'delete-project' || $route == 'forkProject')
            && ($result instanceof JsonModel && $result->isValid)) {
            $spec     = \P4\Spec\Definition::fetch('job', $p4Admin);
            $fields   = $spec->getFields();

            if ($spec->hasField('Project')) {
                $projects = Project::fetchAll(array(), $p4Admin)->toArray();

                $fields['Project']['options'] = array_keys($projects);

                $spec->setFields($fields);
                try {
                    $spec->save();
                } catch (Exception $e) {
                }
            }
        }
    }
}
# Change User Description Committed
#1 18334 Liz Lam initial add of jambox