Browse Source

initial commit: rough out a server-side php endpoint

This is mostly just Composer & Slim framework boilerplate.  There's a bit
of actual code in src/routes.php which uses davechild/textstatistics to
assign Dale-Chall readability scores to page intro extracts.

The need for a framework here at all is, of course, pretty questionable,
but then maybe we already know the initial use case is going to elaborate
rapidly.
Brennen Bearnes 11 months ago
commit
05e6ec5355
14 changed files with 2220 additions and 0 deletions
  1. 2
    0
      .gitignore
  2. 1
    0
      README.md
  3. 40
    0
      composer.json
  4. 1868
    0
      composer.lock
  5. 7
    0
      phpunit.xml
  6. 21
    0
      public/.htaccess
  7. 26
    0
      public/index.php
  8. 19
    0
      src/dependencies.php
  9. 4
    0
      src/middleware.php
  10. 74
    0
      src/routes.php
  11. 21
    0
      src/settings.php
  12. 20
    0
      templates/index.phtml
  13. 77
    0
      tests/Functional/BaseTestCase.php
  14. 40
    0
      tests/Functional/HomepageTest.php

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+/vendor/
2
+/logs/*

+ 1
- 0
README.md View File

@@ -0,0 +1 @@
1
+# MediaWiki Page Readability by Category

+ 40
- 0
composer.json View File

@@ -0,0 +1,40 @@
1
+{
2
+    "name": "brennen/mw-category-readability",
3
+    "description": "A small web application to display readability of pages from a MediaWiki category.",
4
+    "keywords": ["mediawiki", "readability"],
5
+    "homepage": "https://p1k3.com/",
6
+    "license": "Public Domain",
7
+    "authors": [
8
+        {
9
+            "name": "Brennen Bearnes",
10
+            "email": "mediawiki@chaff.p1k3.com",
11
+            "homepage": "https://p1k3.com/"
12
+        }
13
+    ],
14
+    "require": {
15
+        "php": ">=5.5.0",
16
+        "slim/slim": "^3.1",
17
+        "slim/php-view": "^2.0",
18
+        "monolog/monolog": "^1.17",
19
+        "davechild/textstatistics": "1.*"
20
+    },
21
+    "require-dev": {
22
+        "phpunit/phpunit": ">=4.8 < 6.0"
23
+    },
24
+    "autoload": {
25
+      "psr-4": {"MwCategoryReadability\\": "src/"}
26
+    },
27
+    "autoload-dev": {
28
+        "psr-4": {
29
+            "Tests\\": "tests/"
30
+        }
31
+    },
32
+    "config": {
33
+        "process-timeout" : 0
34
+    },
35
+    "scripts": {
36
+        "start": "php -S localhost:8080 -t public",
37
+        "test": "phpunit"
38
+    }
39
+
40
+}

+ 1868
- 0
composer.lock
File diff suppressed because it is too large
View File


+ 7
- 0
phpunit.xml View File

@@ -0,0 +1,7 @@
1
+<phpunit bootstrap="vendor/autoload.php">
2
+    <testsuites>
3
+        <testsuite name="SlimSkeleton">
4
+            <directory>tests</directory>
5
+        </testsuite>
6
+    </testsuites>
7
+</phpunit>

+ 21
- 0
public/.htaccess View File

@@ -0,0 +1,21 @@
1
+<IfModule mod_rewrite.c>
2
+  RewriteEngine On
3
+
4
+  # Some hosts may require you to use the `RewriteBase` directive.
5
+  # Determine the RewriteBase automatically and set it as environment variable.
6
+  # If you are using Apache aliases to do mass virtual hosting or installed the
7
+  # project in a subdirectory, the base path will be prepended to allow proper
8
+  # resolution of the index.php file and to redirect to the correct URI. It will
9
+  # work in environments without path prefix as well, providing a safe, one-size
10
+  # fits all solution. But as you do not need it in this case, you can comment
11
+  # the following 2 lines to eliminate the overhead.
12
+  RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
13
+  RewriteRule ^(.*) - [E=BASE:%1]
14
+  
15
+  # If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the
16
+  # absolute physical path to the directory that contains this htaccess file.
17
+  # RewriteBase /
18
+
19
+  RewriteCond %{REQUEST_FILENAME} !-f
20
+  RewriteRule ^ index.php [QSA,L]
21
+</IfModule>

+ 26
- 0
public/index.php View File

@@ -0,0 +1,26 @@
1
+<?php
2
+if (PHP_SAPI == 'cli-server') {
3
+  // To help the built-in PHP dev server, check if the request was actually for
4
+  // something which should probably be served as a static file
5
+  $url  = parse_url($_SERVER['REQUEST_URI']);
6
+  $file = __DIR__ . $url['path'];
7
+  if (is_file($file)) {
8
+    return false;
9
+  }
10
+}
11
+
12
+require __DIR__ . '/../vendor/autoload.php';
13
+
14
+session_start();
15
+
16
+// Instantiate the app
17
+$settings = require __DIR__ . '/../src/settings.php';
18
+$app = new \Slim\App( $settings );
19
+
20
+// Set up dependencies, register middleware, register routes:
21
+require __DIR__ . '/../src/dependencies.php';
22
+require __DIR__ . '/../src/middleware.php';
23
+require __DIR__ . '/../src/routes.php';
24
+
25
+// Run app
26
+$app->run();

+ 19
- 0
src/dependencies.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+// DIC configuration
3
+
4
+$container = $app->getContainer();
5
+
6
+// view renderer
7
+$container['renderer'] = function ($c) {
8
+  $settings = $c->get('settings')['renderer'];
9
+  return new Slim\Views\PhpRenderer($settings['template_path']);
10
+};
11
+
12
+// monolog
13
+$container['logger'] = function ($c) {
14
+  $settings = $c->get('settings')['logger'];
15
+  $logger = new Monolog\Logger($settings['name']);
16
+  $logger->pushProcessor(new Monolog\Processor\UidProcessor());
17
+  $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
18
+  return $logger;
19
+};

+ 4
- 0
src/middleware.php View File

@@ -0,0 +1,4 @@
1
+<?php
2
+// Application middleware
3
+
4
+// e.g: $app->add(new \Slim\Csrf\Guard);

+ 74
- 0
src/routes.php View File

@@ -0,0 +1,74 @@
1
+<?php
2
+
3
+use DaveChild\TextStatistics as TS;
4
+use Slim\Http\Request;
5
+use Slim\Http\Response;
6
+
7
+// Define routes:
8
+
9
+$app->get( '/', function (Request $request, Response $response, array $args) {
10
+  // Sample log message
11
+  $this->logger->info("Slim-Skeleton '/' route");
12
+
13
+  // Render index view
14
+  return $this->renderer->render( $response, 'index.phtml', $args );
15
+} );
16
+
17
+// An endpoint for getting category data by handing a request off to a
18
+// MediaWiki server:
19
+$app->get( '/category', function (Request $request, Response $response, array $args) {
20
+  $category = $request->getQueryParam('cat');
21
+
22
+  $queryParams = [
23
+    'action'      => 'query',
24
+    'format'      => 'json',
25
+    'generator'   => 'categorymembers',
26
+    'gcmtitle'    => "Category:$category",
27
+    'gcmlimit'    => '50',
28
+    'prop'        => 'extracts',
29
+
30
+    // TODO: Intro seems unlikely to work for all pages:
31
+    'exintro'     => '1',
32
+
33
+    'explaintext' => '1',
34
+  ];
35
+
36
+  $settings = $this->get( 'settings' );
37
+  $queryString = http_build_query( $queryParams );
38
+
39
+  $ch = curl_init( $settings['mwEndpoint'] . $queryString );
40
+
41
+  curl_setopt( $ch, \CURLOPT_RETURNTRANSFER, 1 );
42
+
43
+  $apiResponse = curl_exec( $ch );
44
+  $apiStatus = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
45
+  curl_close( $ch );
46
+
47
+  $responseData = [ 'error' => null ];
48
+
49
+  if ( $apiStatus !== 200 ) {
50
+    $responseData['error'] = 'MediaWiki API request failed.';
51
+  }
52
+
53
+  $categoryData = json_decode( $apiResponse, true );
54
+
55
+  if ( !isset( $categoryData['query']['pages'] ) ) {
56
+    $responseData['error'] = 'No pages found for category.';
57
+  }
58
+
59
+  // Assign readability scores to each extract and build a list:
60
+  $pageList = [];
61
+  $textStatistics = new TS\TextStatistics;
62
+  foreach ( $categoryData['query']['pages'] as $page ) {
63
+    $flesch = $textStatistics->daleChallReadabilityScore( $page['extract'] );
64
+    $pageList[] = [ $page['title'], $page['extract'], $flesch ];
65
+  }
66
+
67
+  // Perform initial sort by readability score:
68
+  usort( $pageList, function ($pageA, $pageB) {
69
+    return $pageA[2] <=> $pageB[2];
70
+  } );
71
+
72
+  $responseData['pagelist'] = $pageList;
73
+  return $response->withJson( $responseData );
74
+} );

+ 21
- 0
src/settings.php View File

@@ -0,0 +1,21 @@
1
+<?php
2
+return [
3
+  'settings' => [
4
+    'displayErrorDetails' => true, // set to false in production
5
+    'addContentLengthHeader' => false, // Allow the web server to send the content-length header
6
+
7
+    // Renderer settings
8
+    'renderer' => [
9
+      'template_path' => __DIR__ . '/../templates/',
10
+    ],
11
+
12
+    // Monolog settings
13
+    'logger' => [
14
+      'name' => 'slim-app',
15
+      'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
16
+      'level' => \Monolog\Logger::DEBUG,
17
+    ],
18
+
19
+    'mwEndpoint' => 'https://en.wikipedia.org/w/api.php?'
20
+  ],
21
+];

+ 20
- 0
templates/index.phtml View File

@@ -0,0 +1,20 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+  <meta charset="utf-8"/>
5
+  <title>Readability</title>
6
+  <link href="" rel="stylesheet" type="text/css">
7
+</head>
8
+
9
+<body>
10
+
11
+  <h1>Readability</h1>
12
+
13
+  <form action="/category" method="get">
14
+    <label for="category-name">Category:</label>
15
+    <input type="text" id="category-name" name="cat">
16
+  </form>
17
+
18
+</body>
19
+
20
+</html>

+ 77
- 0
tests/Functional/BaseTestCase.php View File

@@ -0,0 +1,77 @@
1
+<?php
2
+
3
+namespace Tests\Functional;
4
+
5
+use Slim\App;
6
+use Slim\Http\Request;
7
+use Slim\Http\Response;
8
+use Slim\Http\Environment;
9
+
10
+/**
11
+ * This is an example class that shows how you could set up a method that
12
+ * runs the application. Note that it doesn't cover all use-cases and is
13
+ * tuned to the specifics of this skeleton app, so if your needs are
14
+ * different, you'll need to change it.
15
+ */
16
+class BaseTestCase extends \PHPUnit_Framework_TestCase
17
+{
18
+    /**
19
+     * Use middleware when running application?
20
+     *
21
+     * @var bool
22
+     */
23
+    protected $withMiddleware = true;
24
+
25
+    /**
26
+     * Process the application given a request method and URI
27
+     *
28
+     * @param string $requestMethod the request method (e.g. GET, POST, etc.)
29
+     * @param string $requestUri the request URI
30
+     * @param array|object|null $requestData the request data
31
+     * @return \Slim\Http\Response
32
+     */
33
+    public function runApp($requestMethod, $requestUri, $requestData = null)
34
+    {
35
+        // Create a mock environment for testing with
36
+        $environment = Environment::mock(
37
+            [
38
+                'REQUEST_METHOD' => $requestMethod,
39
+                'REQUEST_URI' => $requestUri
40
+            ]
41
+        );
42
+
43
+        // Set up a request object based on the environment
44
+        $request = Request::createFromEnvironment($environment);
45
+
46
+        // Add request data, if it exists
47
+        if (isset($requestData)) {
48
+            $request = $request->withParsedBody($requestData);
49
+        }
50
+
51
+        // Set up a response object
52
+        $response = new Response();
53
+
54
+        // Use the application settings
55
+        $settings = require __DIR__ . '/../../src/settings.php';
56
+
57
+        // Instantiate the application
58
+        $app = new App($settings);
59
+
60
+        // Set up dependencies
61
+        require __DIR__ . '/../../src/dependencies.php';
62
+
63
+        // Register middleware
64
+        if ($this->withMiddleware) {
65
+            require __DIR__ . '/../../src/middleware.php';
66
+        }
67
+
68
+        // Register routes
69
+        require __DIR__ . '/../../src/routes.php';
70
+
71
+        // Process the application
72
+        $response = $app->process($request, $response);
73
+
74
+        // Return the response
75
+        return $response;
76
+    }
77
+}

+ 40
- 0
tests/Functional/HomepageTest.php View File

@@ -0,0 +1,40 @@
1
+<?php
2
+
3
+namespace Tests\Functional;
4
+
5
+class HomepageTest extends BaseTestCase
6
+{
7
+    /**
8
+     * Test that the index route returns a rendered response containing the text 'SlimFramework' but not a greeting
9
+     */
10
+    public function testGetHomepageWithoutName()
11
+    {
12
+        $response = $this->runApp('GET', '/');
13
+
14
+        $this->assertEquals(200, $response->getStatusCode());
15
+        $this->assertContains('SlimFramework', (string)$response->getBody());
16
+        $this->assertNotContains('Hello', (string)$response->getBody());
17
+    }
18
+
19
+    /**
20
+     * Test that the index route with optional name argument returns a rendered greeting
21
+     */
22
+    public function testGetHomepageWithGreeting()
23
+    {
24
+        $response = $this->runApp('GET', '/name');
25
+
26
+        $this->assertEquals(200, $response->getStatusCode());
27
+        $this->assertContains('Hello name!', (string)$response->getBody());
28
+    }
29
+
30
+    /**
31
+     * Test that the index route won't accept a post request
32
+     */
33
+    public function testPostHomepageNotAllowed()
34
+    {
35
+        $response = $this->runApp('POST', '/', ['test']);
36
+
37
+        $this->assertEquals(405, $response->getStatusCode());
38
+        $this->assertContains('Method not allowed', (string)$response->getBody());
39
+    }
40
+}

Loading…
Cancel
Save