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 1 year 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